mirror of
https://github.com/expo/turtle.git
synced 2026-11-14 19:12:55 -10:00
Compare commits
137
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6d8726a1fc | ||
|
|
1d773f35cf | ||
|
|
3daa678540 | ||
|
|
616331b47b | ||
|
|
0b9ab78453 | ||
|
|
3e345ddaf8 | ||
|
|
4866c06a02 | ||
|
|
c36723d96c | ||
|
|
28b0266ba2 | ||
|
|
84a19ee261 | ||
|
|
8f3be65326 | ||
|
|
5d539a1bb2 | ||
|
|
833c9c1de5 | ||
|
|
ce74ab3161 | ||
|
|
62df247645 | ||
|
|
9bc4d75e9b | ||
|
|
b4d4b8a6c3 | ||
|
|
88d75c7581 | ||
|
|
03be505cd0 | ||
|
|
05568676ee | ||
|
|
903e44bd29 | ||
|
|
7697542b44 | ||
|
|
3baf4bcade | ||
|
|
c7e98c5858 | ||
|
|
fc48d27e42 | ||
|
|
fdc702f273 | ||
|
|
9fda74dfa4 | ||
|
|
6bab5f71ef | ||
|
|
3f1e9ce910 | ||
|
|
8fb54f3f27 | ||
|
|
069ef7e1c9 | ||
|
|
c0a742fa21 | ||
|
|
fca72875dc | ||
|
|
15ed26d4cd | ||
|
|
4c7af801f1 | ||
|
|
5012ec669d | ||
|
|
8b77a48b82 | ||
|
|
c6946eca25 | ||
|
|
9627ecb80c | ||
|
|
48c78f386a | ||
|
|
0dc251e023 | ||
|
|
d984061134 | ||
|
|
ff6401ba6b | ||
|
|
b443760699 | ||
|
|
0f0e86d17f | ||
|
|
f6697c0029 | ||
|
|
22f802164d | ||
|
|
aaa6e61061 | ||
|
|
e532e0270c | ||
|
|
bc0718ca7d | ||
|
|
bf63dcea70 | ||
|
|
9551c8773d | ||
|
|
470ff609fd | ||
|
|
addba74b9d | ||
|
|
b63e9e3f34 | ||
|
|
25aaa849f1 | ||
|
|
edc712d820 | ||
|
|
23c3e3509c | ||
|
|
406a3be4f1 | ||
|
|
dd60721270 | ||
|
|
469ead8df8 | ||
|
|
aa0ce81e98 | ||
|
|
dccccb7716 | ||
|
|
57f8404b6d | ||
|
|
ba7309bc4b | ||
|
|
4aec3d5ba7 | ||
|
|
d2eb447ebc | ||
|
|
f177cf4d7f | ||
|
|
59a181dee0 | ||
|
|
f4bf860a9a | ||
|
|
932958ad46 | ||
|
|
2a54b4578a | ||
|
|
7e234d09a3 | ||
|
|
5b22c351bf | ||
|
|
e2020f1ab4 | ||
|
|
c935e53e50 | ||
|
|
14e681f35f | ||
|
|
d3a7ccb582 | ||
|
|
62478de14a | ||
|
|
6594089779 | ||
|
|
11c5d3ee02 | ||
|
|
99b7ac6660 | ||
|
|
46fe34ed9c | ||
|
|
423fc8bbf5 | ||
|
|
4c8408a46b | ||
|
|
032d7c8b1f | ||
|
|
b2a0b3ce14 | ||
|
|
1ee922763e | ||
|
|
c31cd379c1 | ||
|
|
67754eba2a | ||
|
|
351fed00df | ||
|
|
487032b11d | ||
|
|
042dd2ccab | ||
|
|
99ae0d1cea | ||
|
|
ad2c87163b | ||
|
|
b4b9cf362e | ||
|
|
7ea4dac986 | ||
|
|
cd8da71711 | ||
|
|
2a2a0f39e0 | ||
|
|
fbabcdf521 | ||
|
|
0803811226 | ||
|
|
0976a642e1 | ||
|
|
16bd4fe7b6 | ||
|
|
3ccd705943 | ||
|
|
11cc4421dd | ||
|
|
32f5a0bb37 | ||
|
|
61a1906e01 | ||
|
|
19a17231b3 | ||
|
|
98c03bc8a2 | ||
|
|
83ab2539c1 | ||
|
|
064619e1ae | ||
|
|
b7fe01b758 | ||
|
|
6583b1a1db | ||
|
|
f24081cb91 | ||
|
|
4e719a200b | ||
|
|
0b13494402 | ||
|
|
0ca26a40aa | ||
|
|
f171913bac | ||
|
|
b47ee91117 | ||
|
|
77cb28ce66 | ||
|
|
7a2453cf53 | ||
|
|
924908905f | ||
|
|
798b4acca6 | ||
|
|
2303b44dd2 | ||
|
|
c204e0bef2 | ||
|
|
0918d8299e | ||
|
|
3543acfb26 | ||
|
|
42b6ff634c | ||
|
|
fcd42b272f | ||
|
|
ecaef3a91d | ||
|
|
413e6a564d | ||
|
|
ac27332151 | ||
|
|
72744f58ec | ||
|
|
96af4f753d | ||
|
|
4059c6f354 | ||
|
|
b48f89054b | ||
|
|
7361f4af52 |
No files matched your search
+13
-13
@@ -9,7 +9,7 @@ executors:
|
||||
working_directory: ~/turtle
|
||||
mac:
|
||||
macos: # https://circleci.com/docs/2.0/testing-ios/#supported-xcode-versions
|
||||
xcode: '11.1.0'
|
||||
xcode: "11.1.0"
|
||||
working_directory: /Users/distiller/project
|
||||
shell: /bin/bash -leo pipefail
|
||||
android:
|
||||
@@ -57,8 +57,8 @@ commands:
|
||||
- run:
|
||||
name: Configure aws-cli
|
||||
command: |
|
||||
aws configure set aws_access_key_id $AWS_ACCESS_KEY_ID
|
||||
aws configure set aws_secret_access_key $AWS_SECRET_ACCESS_KEY
|
||||
aws configure set aws_access_key_id $AWS_ACCESS_KEY_ID
|
||||
aws configure set aws_secret_access_key $AWS_SECRET_ACCESS_KEY
|
||||
|
||||
setup_nix:
|
||||
steps:
|
||||
@@ -141,7 +141,7 @@ commands:
|
||||
default: linux
|
||||
steps:
|
||||
- restore_cache:
|
||||
key: yarn-v2-<< parameters.platform >>-{{ checksum "<< parameters.workingdir >>/yarn.lock" }}
|
||||
key: yarn-v3-<< parameters.platform >>-{{ checksum "<< parameters.workingdir >>/yarn.lock" }}
|
||||
|
||||
save_yarn_cache:
|
||||
parameters:
|
||||
@@ -153,7 +153,7 @@ commands:
|
||||
default: linux
|
||||
steps:
|
||||
- save_cache:
|
||||
key: yarn-v2-<< parameters.platform >>-{{ checksum "<< parameters.workingdir >>/yarn.lock" }}
|
||||
key: yarn-v3-<< parameters.platform >>-{{ checksum "<< parameters.workingdir >>/yarn.lock" }}
|
||||
paths:
|
||||
- ~/.cache/yarn # `yarn cache dir`
|
||||
|
||||
@@ -164,7 +164,7 @@ commands:
|
||||
default: ios
|
||||
sdk_version:
|
||||
type: string
|
||||
default: sdk37
|
||||
default: sdk39
|
||||
steps:
|
||||
- restore_cache:
|
||||
key: "workingdir-v2-<< parameters.platform >>-<< parameters.sdk_version >>"
|
||||
@@ -179,7 +179,7 @@ commands:
|
||||
default: ios
|
||||
sdk_version:
|
||||
type: string
|
||||
default: sdk37
|
||||
default: sdk39
|
||||
steps:
|
||||
- save_cache:
|
||||
key: "workingdir-v2-<< parameters.platform >>-<< parameters.sdk_version >>"
|
||||
@@ -238,14 +238,14 @@ jobs:
|
||||
- save_yarn_cache:
|
||||
platform: darwin
|
||||
- restore_workingdir_cache:
|
||||
sdk_version: sdk37
|
||||
sdk_version: sdk39
|
||||
platform: ios
|
||||
- run:
|
||||
name: Run iOS smoke tests
|
||||
command: SDK_VERSION=sdk37 ./scripts/tests/runIOSSmokeTestsCI.sh
|
||||
command: SDK_VERSION=sdk39 ./scripts/tests/runIOSSmokeTestsCI.sh
|
||||
- save_workingdir_cache:
|
||||
workingdir: /Users/distiller/project/workingdir
|
||||
sdk_version: sdk37
|
||||
sdk_version: sdk39
|
||||
platform: ios
|
||||
|
||||
android_smoke_tests:
|
||||
@@ -257,11 +257,11 @@ jobs:
|
||||
- run: yarn
|
||||
- save_yarn_cache
|
||||
- restore_workingdir_cache:
|
||||
sdk_version: sdk37
|
||||
sdk_version: sdk39
|
||||
platform: android
|
||||
- run:
|
||||
name: Fetch Android shell app
|
||||
command: SDK_VERSION=sdk37 ./scripts/android/fetchRemoteAndroidTarball.sh
|
||||
command: SDK_VERSION=sdk39 ./scripts/android/fetchRemoteAndroidTarball.sh
|
||||
- run:
|
||||
name: Run Android smoke tests
|
||||
command: |
|
||||
@@ -269,7 +269,7 @@ jobs:
|
||||
--run "./scripts/tests/runAndroidSmokeTestsCI.sh"
|
||||
- save_workingdir_cache:
|
||||
workingdir: ~/turtle/workingdir
|
||||
sdk_version: sdk37
|
||||
sdk_version: sdk39
|
||||
platform: android
|
||||
|
||||
android_staging_deploy:
|
||||
|
||||
@@ -6,6 +6,7 @@ google-cloud-credentials.json
|
||||
/workingdir/*
|
||||
/artifacts/*
|
||||
.history
|
||||
.vscode/
|
||||
|
||||
shell.apk
|
||||
.secrets*
|
||||
|
||||
@@ -10,6 +10,9 @@
|
||||
"release": true
|
||||
},
|
||||
"hooks": {
|
||||
"before:init": [
|
||||
"yarn"
|
||||
],
|
||||
"after:release": "yarn release:update:turtle-cli-example"
|
||||
}
|
||||
}
|
||||
+220
@@ -7,6 +7,226 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## Unreleased
|
||||
|
||||
- Updated SDK 40 Android, iOS, and client tarballs for patch release.
|
||||
|
||||
# [0.20.6] - 2021-01-25
|
||||
|
||||
- Update tarballs for SDK 40 patch release (3e345dd)
|
||||
|
||||
# [0.20.5] - 2020-12-23
|
||||
|
||||
### Changed
|
||||
|
||||
- Updated SDK 40 Android tarball to fix `expo-location` background permissions on Android <9.
|
||||
|
||||
# [0.20.4] - 2020-12-17
|
||||
|
||||
### Changed
|
||||
|
||||
- Updated SDK 40 iOS tarball to fix `expo-notifications` not emitting initial notification response.
|
||||
|
||||
# [0.20.3] - 2020-12-10
|
||||
|
||||
### Fixed
|
||||
|
||||
- Verify jq installation before proceeding with release ([#281](https://github.com/expo/turtle/pull/281)).
|
||||
- Fixed the iOS build when running offline and user lacks elevated permissions.
|
||||
|
||||
# [0.20.2] - 2020-12-09
|
||||
|
||||
### Changed
|
||||
|
||||
- Updated SDK 40 Android tarball to fix `expo-notifications` installation identifier changing as a bug.
|
||||
|
||||
# [0.20.1] - 2020-12-08
|
||||
|
||||
### Changed
|
||||
|
||||
- Update SDK 40 iOS and Android tarballs for release
|
||||
|
||||
# [0.20.0] - 2020-11-30
|
||||
|
||||
- Add SDK 40 tarballs in preparation for SDK 40 beta
|
||||
|
||||
# [0.19.1] - 2020-11-19
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed the iOS build using Xcode 12.1 by runnig `xcrun simctl list` before the build phase.
|
||||
- Rebuild iOS tarballs (SDK 36, 37, 38, 39, Ad-Hoc Client) with Xcode 12.1 containing iOS 14 image fix.
|
||||
|
||||
# [0.19.0] - 2020-11-13
|
||||
|
||||
### Changed
|
||||
|
||||
- Rebuild iOS tarballs (SDK 36, 37, 38, 39, Ad-Hoc Client) with Xcode 12.1.
|
||||
|
||||
# [0.18.9] - 2020-11-09
|
||||
|
||||
### Fixed
|
||||
|
||||
- Updated xdl to 58.0.20 to fix an issue with updates.checkAutomatically not being applied properly to SDK 39 Android apps.
|
||||
|
||||
# [0.18.8] - 2020-10-27
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix issues with Reanimated v2 in Android SDK 39 tarball.
|
||||
|
||||
# [0.18.7] - 2020-10-23
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix issue with updates in self-hosted Android apps ([#10746](https://github.com/expo/expo/issues/10746)) in the SDK 39 tarball
|
||||
|
||||
# [0.18.6] - 2020-10-22
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix issues with notifications ([10624](https://github.com/expo/expo/pull/10624) and [10608](https://github.com/expo/expo/pull/10608)) in the Android SDK 39 tarball.
|
||||
|
||||
# [0.18.5] - 2020-10-07
|
||||
|
||||
### Fixed
|
||||
|
||||
- Always add expo-notifications for SDK >= 39 projects with android.enableDangerousExperimentalLeanBuilds enabled.
|
||||
|
||||
# [0.18.4] - 2020-10-06
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed an issue with notifications ([expo/expo#10562](https://github.com/expo/expo/issues/10562)) in the Android SDK 39 tarball.
|
||||
|
||||
# [0.18.3] - 2020-10-03
|
||||
|
||||
### Fixed
|
||||
|
||||
- Update @expo/xdl to fix regression in SDK <= 39 splash
|
||||
|
||||
# [0.18.2] - 2020-10-03
|
||||
|
||||
### Fixed
|
||||
|
||||
- Updated SDK 39 Android tarball.
|
||||
- Updated SDK 39 iOS tarball.
|
||||
- Updated iOS client tarball.
|
||||
- Update @expo/config and @expo/xdl. Temporarily bring in type definitions for color-string and xcode.
|
||||
|
||||
## [0.18.1] - 2020-09-22
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix building apps using SDK 39 for Android.
|
||||
|
||||
## [0.18.0] - 2020-09-21
|
||||
|
||||
### Added
|
||||
|
||||
- Added iOS and Android shell apps for SDK 39.
|
||||
|
||||
### Removed
|
||||
|
||||
- Dropped support for SDK 35.
|
||||
|
||||
## [0.17.3] - 2020-09-14
|
||||
|
||||
### Added
|
||||
|
||||
- Added gradle 6.2 to `turtle-android-base`
|
||||
- Added `ANDROID_NDK_HOME` in `turtle-android-base`
|
||||
|
||||
### Changed
|
||||
|
||||
- Upgraded XDL so it processes root `build.gradle` of Android shell apps too
|
||||
|
||||
## [0.17.2] - 2020-08-06
|
||||
|
||||
### Changed
|
||||
|
||||
- Updated iOS shell app for SDK 38 with fixed integer tagging in expo-gl for iOS 14. ([#245](https://github.com/expo/turtle/pull/245))
|
||||
|
||||
## [0.17.1] - 2020-08-05
|
||||
|
||||
### Changed
|
||||
|
||||
- Updated Android shell app for SDK 38 with `targetSdkVersion` bumped to 29. ([#244](https://github.com/expo/turtle/pull/244))
|
||||
|
||||
## [0.17.0] - 2020-07-16
|
||||
|
||||
### Added
|
||||
|
||||
- `--gradle-args` option for `turtle build:android` which makes it possible to specify custom Gradle arguments.
|
||||
|
||||
### Changed
|
||||
|
||||
- Gradle Wrapper doesn't print when dots when the appropriate version of Gradle is being downloaded.
|
||||
|
||||
## [0.16.2] - 2020-07-01
|
||||
|
||||
### Added
|
||||
|
||||
- Added `--allow-non-https-public-url` to allow bypass of protocol validation on `--public-url`.
|
||||
- Added support for dynamic configs (`app.config.ts`, `app.config.js`).
|
||||
|
||||
## [0.16.0] - 2020-06-25
|
||||
|
||||
### Added
|
||||
|
||||
- Added iOS shell app for Expo SDK38.
|
||||
- Added Android shell app for SDK 38.
|
||||
|
||||
### Removed
|
||||
|
||||
- Dropped support for SDK 34.
|
||||
|
||||
## [0.15.1] - 2020-06-22
|
||||
|
||||
### Changed
|
||||
|
||||
- Configure Turtle to use the Redis CA certificate.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix announcing supported SDK versions.
|
||||
|
||||
## [0.15.0] - 2020-06-02
|
||||
|
||||
### Changed
|
||||
|
||||
- Updated `@expo/xdl` to 57.9.13 (added \*.bak files clean-up when building .ipa).
|
||||
|
||||
## [0.14.12] - 2020-05-14
|
||||
|
||||
### Changed
|
||||
|
||||
- Update iOS custom client shell app with various fixes.
|
||||
|
||||
## [0.14.11] - 2020-05-04
|
||||
|
||||
### Changed
|
||||
|
||||
- Update SDK 37 iOS shell app with various fixes.
|
||||
|
||||
## [0.14.10] - 2020-05-01
|
||||
|
||||
### Added
|
||||
|
||||
- support for "android.enableDangerousExperimentalLeanBuilds" in app.json.
|
||||
|
||||
## [0.14.9] - 2020-04-21
|
||||
|
||||
### Changed
|
||||
|
||||
- Updated `@expo/xdl` to 57.8.30 (scoped the build command so it only builds the `:app` Android sub-project and not all the sub-projects available, [PR](https://github.com/expo/expo-cli/pull/1937)).
|
||||
|
||||
## [0.14.8] - 2020-04-20
|
||||
|
||||
### Changed
|
||||
|
||||
- Updated `@expo/xdl` to 57.8.29 (removed call to `check-dynamic-macros-android.sh`, [PR](https://github.com/expo/expo-cli/pull/1933)).
|
||||
|
||||
## [0.14.7] - 2020-04-20
|
||||
|
||||
### Changed
|
||||
|
||||
- Turtle no longer installs all dependencies of shell apps projects — only the production dependencies are installed now.
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@
|
||||
## Setup
|
||||
|
||||
- Run `update-local-secrets` to populate the `.secrets*` files. You'll need to run this any time any secret changes (it shouldn't be very often).
|
||||
- Run `EXPO_UNIVERSE_DIR=/path/to/universe/dir yarn secrets:init-private YOUR_SQS_NICKNAME`. Alternatively, you can create `.secrets.local-queue` file with the SQS queues' urls yourself (`AWS_SQS_OUT_QUEUE_URL`, `AWS_SQS_ANDROID_QUEUE_URL`, `AWS_SQS_ANDROID_PRIORITY_QUEUE_URL`, `AWS_SQS_IOS_QUEUE_URL`, `AWS_SQS_IOS_PRIORITY_QUEUE_URL`)
|
||||
- Run `EXPO_UNIVERSE_DIR=/path/to/universe/dir yarn secrets:init-private YOUR_SQS_NICKNAME`. Alternatively, you can create `.secrets.local-queue` file with the SQS queues' urls yourself (`AWS_SQS_ANDROID_QUEUE_URL`, `AWS_SQS_ANDROID_PRIORITY_QUEUE_URL`, `AWS_SQS_IOS_QUEUE_URL`, `AWS_SQS_IOS_PRIORITY_QUEUE_URL`)
|
||||
- Run `yarn set-workingdir:local` or `yarn set-workingdir:remote`:
|
||||
- `:local` creates symlinks to the shell apps in `expo` repository (_unfortunately this is currently broken_)
|
||||
- `:remote` downloads precompiled shell apps
|
||||
|
||||
+12
-11
@@ -1,5 +1,5 @@
|
||||
# see docker-base/Dockerfile to learn what's installed in the turtle-android-base image
|
||||
FROM gcr.io/exponentjs/turtle-android-base:0.4.0
|
||||
FROM gcr.io/exponentjs/turtle-android-base:0.5.0
|
||||
|
||||
ADD . /app/turtle
|
||||
|
||||
@@ -8,20 +8,21 @@ WORKDIR /app/turtle
|
||||
RUN ./scripts/fetchRemoteTarballs.sh android
|
||||
|
||||
RUN ln -s /app/turtle/workingdir /app/workingdir && \
|
||||
for SDK_VERSION in `ls /app/workingdir/android/`; do \
|
||||
echo "preparing $SDK_VERSION shell app" && \
|
||||
cd /app/workingdir/android/$SDK_VERSION && \
|
||||
mv packages non-workspace-packages && \
|
||||
# Keep in sync with src/bin/setup/android/index.ts#_installNodeModules
|
||||
# --prod requires --ignore-scripts (otherwise
|
||||
# expo-yarn-workspaces errors as missing)
|
||||
yarn install --ignore-scripts --prod && \
|
||||
mv non-workspace-packages packages ; \
|
||||
done
|
||||
for SDK_VERSION in `ls /app/workingdir/android/`; do \
|
||||
echo "preparing $SDK_VERSION shell app" && \
|
||||
cd /app/workingdir/android/$SDK_VERSION && \
|
||||
mv packages non-workspace-packages && \
|
||||
# Keep in sync with src/bin/setup/android/index.ts#_installNodeModules
|
||||
# --prod requires --ignore-scripts (otherwise
|
||||
# expo-yarn-workspaces errors as missing)
|
||||
yarn install --ignore-scripts --prod && \
|
||||
mv non-workspace-packages packages ; \
|
||||
done
|
||||
|
||||
ENV NODE_ENV "production"
|
||||
ENV TURTLE_WORKING_DIR_PATH /app/turtle/workingdir/
|
||||
|
||||
ADD supervisord.conf /etc/supervisor/conf.d/supervisord.conf
|
||||
ADD redislabs_ca.pem /var/run/config/redislabs-tls/redislabs_ca.pem
|
||||
|
||||
CMD ["/usr/bin/supervisord"]
|
||||
@@ -21,7 +21,7 @@ You can use it both on your CI and your private computer.
|
||||
|
||||
## Installation
|
||||
|
||||
[Installation instructions and documentation here.](https://docs.expo.io/versions/latest/distribution/turtle-cli)
|
||||
[Installation instructions and documentation here.](https://docs.expo.io/distribution/turtle-cli/)
|
||||
|
||||
## Usage example
|
||||
|
||||
|
||||
@@ -57,10 +57,23 @@ ENV PATH ${ANDROID_HOME}/build-tools/28.0.3/:${PATH}
|
||||
|
||||
ADD configureAndroidSdk.sh /tmp/configureAndroidSdk.sh
|
||||
RUN /tmp/configureAndroidSdk.sh --install-all-platforms
|
||||
ENV ANDROID_NDK_HOME /usr/local/android-sdk-linux/ndk/17.2.4988734
|
||||
|
||||
# Support Gradle
|
||||
ENV TERM dumb
|
||||
|
||||
# Install gradle 6.2
|
||||
RUN wget https://services.gradle.org/distributions/gradle-6.2-all.zip
|
||||
RUN unzip -qq gradle-6.2-all.zip
|
||||
RUN cp -r gradle-6.2 /usr/local
|
||||
|
||||
RUN mkdir -p /root/.gradle/wrapper/dists/gradle-6.2-all/dvufqs6kielxeao781pmk5huj
|
||||
RUN mv gradle-6.2-all.zip /root/.gradle/wrapper/dists/gradle-6.2-all/dvufqs6kielxeao781pmk5huj
|
||||
RUN mv gradle-6.2 /root/.gradle/wrapper/dists/gradle-6.2-all/dvufqs6kielxeao781pmk5huj
|
||||
RUN touch /root/.gradle/wrapper/dists/gradle-6.2-all/dvufqs6kielxeao781pmk5huj/gradle-6.2-all.zip.ok
|
||||
RUN touch /root/.gradle/wrapper/dists/gradle-6.2-all/dvufqs6kielxeao781pmk5huj/gradle-6.2-all.zip.lck
|
||||
RUN chmod 755 /root/.gradle/wrapper/dists/gradle-6.2-all/dvufqs6kielxeao781pmk5huj/gradle-6.2/bin/gradle
|
||||
|
||||
# Install gradle 5.6.3
|
||||
RUN wget https://services.gradle.org/distributions/gradle-5.6.3-all.zip
|
||||
RUN unzip -qq gradle-5.6.3-all.zip
|
||||
|
||||
@@ -88,11 +88,6 @@ spec:
|
||||
secretKeyRef:
|
||||
name: turtle-js-config
|
||||
key: aws_sqs_android_priority_queue_url
|
||||
- name: AWS_SQS_OUT_QUEUE_URL
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: turtle-js-config
|
||||
key: aws_sqs_out_queue_url
|
||||
- name: AWS_CLOUDWATCH_REGION
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
@@ -149,9 +144,11 @@ spec:
|
||||
- name: DISABLE_DEX_MAX_HEAP
|
||||
value: "true"
|
||||
- name: JAVA_OPTS
|
||||
value: '-Xms2048m -XX:+HeapDumpOnOutOfMemoryError -XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap -XX:+UseG1GC'
|
||||
value: '-Xms2048m -Xmx8192m -XX:+HeapDumpOnOutOfMemoryError -XX:+UseG1GC'
|
||||
- name: GRADLE_OPTS
|
||||
value: '-Dorg.gradle.parallel=true -Dorg.gradle.configureondemand=true -Dorg.gradle.daemon=false -Dorg.gradle.jvmargs="-Xms2048m -XX:+HeapDumpOnOutOfMemoryError -XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap -XX:+UseG1GC"'
|
||||
value: '-Dorg.gradle.parallel=true -Dorg.gradle.configureondemand=true -Dorg.gradle.daemon=false -Dorg.gradle.jvmargs="-Xms2048m -Xmx8192m -XX:+HeapDumpOnOutOfMemoryError -XX:+UseG1GC"'
|
||||
- name: REDIS_CA_CERTIFICATE
|
||||
value: '/var/run/config/redislabs-tls/redislabs_ca.pem'
|
||||
resources:
|
||||
requests:
|
||||
memory: "8192Mi"
|
||||
|
||||
+38
-10
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "turtle-cli",
|
||||
"version": "0.14.7",
|
||||
"version": "0.20.7",
|
||||
"description": "The command-line tool for building standalone Expo apps",
|
||||
"preferGlobal": true,
|
||||
"scripts": {
|
||||
@@ -67,14 +67,20 @@
|
||||
"registry": "https://registry.npmjs.org/"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@expo/babel-preset-cli": "^0.1.3",
|
||||
"@expo/babel-preset-cli": "^0.2.18",
|
||||
"@types/color-string": "^1.5.0",
|
||||
"@types/decompress": "^4.2.3",
|
||||
"@types/fs-extra": "^5.0.4",
|
||||
"@types/ioredis": "^4.16.6",
|
||||
"@types/jest": "^23.3.9",
|
||||
"@types/probe-image-size": "^5.0.1",
|
||||
"@types/latest-version": "^4.0.1",
|
||||
"@types/joi": "^13.4.3",
|
||||
"@types/lodash": "^4.14.129",
|
||||
"@types/node-forge": "^0.8.6",
|
||||
"@types/npm-package-arg": "^6.1.0",
|
||||
"@types/progress": "^2.0.1",
|
||||
"@types/ncp": "^2.0.1",
|
||||
"@types/raven": "^2.5.1",
|
||||
"@types/request": "^2.47.1",
|
||||
"@types/request-promise": "^4.1.44",
|
||||
@@ -84,30 +90,40 @@
|
||||
"@types/uuid": "^3.4.3",
|
||||
"@types/webpack": "^4.32.1",
|
||||
"@types/webpack-dev-server": "^3.1.7",
|
||||
"@types/url-join": "^4.0.0",
|
||||
"@types/which": "^1.3.1",
|
||||
"husky": "^1.3.1",
|
||||
"jest": "^25.1.0",
|
||||
"jest": "^26.1.0",
|
||||
"lint-staged": "^8.1.5",
|
||||
"nock": "^10.0.1",
|
||||
"nodemon": "^2.0.2",
|
||||
"release-it": "^12.3.4",
|
||||
"rimraf": "^2.6.2",
|
||||
"ts-jest": "^25.2.1",
|
||||
"ts-jest": "^26.1.2",
|
||||
"ts-node": "^8.0.3",
|
||||
"ts-transformer-imports": "^0.4.1",
|
||||
"tsconfig-paths": "^3.8.0",
|
||||
"tslint": "^5.10.0",
|
||||
"tslint-eslint-rules": "^5.4.0",
|
||||
"ttypescript": "^1.5.6",
|
||||
"typescript": "^3.4.2"
|
||||
"typescript": "^3.9.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"@expo/config": "^3.0.5",
|
||||
"@expo/spawn-async": "^1.4.2",
|
||||
"@expo/xdl": "57.8.25",
|
||||
"@expo/bunyan": "4.0.0",
|
||||
"@expo/config": "~3.3.9",
|
||||
"@expo/spawn-async": "~1.5.0",
|
||||
"@expo/plist": "0.0.11",
|
||||
"@expo/json-file": "8.2.26",
|
||||
"@google-cloud/logging-bunyan": "^2.0.3",
|
||||
"async-retry": "^1.2.1",
|
||||
"aws-sdk": "^2.226.1",
|
||||
"axios": "0.21.1",
|
||||
"ncp": "2.0.0",
|
||||
"minimatch": "3.0.4",
|
||||
"indent-string": "3.2.0",
|
||||
"pascal-case": "2.0.1",
|
||||
"node-forge": "0.7.6",
|
||||
"glob": "7.1.6",
|
||||
"bunyan": "^1.8.12",
|
||||
"bunyan-debug-stream": "^1.0.8",
|
||||
"bunyan-sentry-stream": "^1.2.1",
|
||||
@@ -116,21 +132,33 @@
|
||||
"decompress": "^4.2.0",
|
||||
"dogapi": "^2.8.3",
|
||||
"fs-extra": "^6.0.0",
|
||||
"ioredis": "^3.2.2",
|
||||
"ioredis": "^4.17.3",
|
||||
"is-semver": "^1.0.8",
|
||||
"joi": "^13.2.0",
|
||||
"latest-version": "5.1.0",
|
||||
"lodash": "^4.17.11",
|
||||
"p-map": "3.0.0",
|
||||
"p-retry": "4.1.0",
|
||||
"probe-image-size": "^6.0.0",
|
||||
"p-timeout": "3.1.0",
|
||||
"package-json": "6.4.0",
|
||||
"progress": "^2.0.0",
|
||||
"prompts": "^2.3.2",
|
||||
"raven": "^2.6.2",
|
||||
"request": "^2.88.0",
|
||||
"request-progress": "^3.0.0",
|
||||
"request-promise": "^4.2.5",
|
||||
"replace-string": "1.1.0",
|
||||
"rimraf": "^2.6.2",
|
||||
"semver": "^6.0.0",
|
||||
"sharp": "0.24.1",
|
||||
"source-map-support": "^0.5.6",
|
||||
"tar": "^4.4.6",
|
||||
"url-join": "4.0.0",
|
||||
"uuid": "^3.2.1",
|
||||
"which": "^1.3.1"
|
||||
"which": "^1.3.1",
|
||||
"xcode": "^2.1.0",
|
||||
"xmldom": "0.1.27"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@expo/traveling-fastlane-darwin": "1.11.4"
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
this is just a placeholder to simplify deployments
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
if [ -z "$EXPO_UNIVERSE_DIR" ]; then
|
||||
echo "You need to specify EXPO_UNIVERSE_DIR to use this script. Alternatively you can create .secrets.local-queue file yourself with AWS_SQS_ANDROID_QUEUE_URL, AWS_SQS_IOS_QUEUE_URL and AWS_SQS_OUT_QUEUE_URL variables."
|
||||
echo "You need to specify EXPO_UNIVERSE_DIR to use this script. Alternatively you can create .secrets.local-queue file yourself with AWS_SQS_ANDROID_QUEUE_URL and AWS_SQS_IOS_QUEUE_URL variables."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -17,13 +17,11 @@ ANDROID_NORMAL=`cd $TERRAFORM_DIR; terraform state show module.turtle_queues_loc
|
||||
ANDROID_PRIORITY=`cd $TERRAFORM_DIR; terraform state show module.turtle_queues_local_$1.aws_sqs_queue.jobs_android_in_priority | head -1 | cut -d"=" -f2 | xargs`
|
||||
IOS_NORMAL=`cd $TERRAFORM_DIR; terraform state show module.turtle_queues_local_$1.aws_sqs_queue.jobs_ios_in | head -1 | cut -d"=" -f2 | xargs`
|
||||
IOS_PRIORITY=`cd $TERRAFORM_DIR; terraform state show module.turtle_queues_local_$1.aws_sqs_queue.jobs_ios_in_priority | head -1 | cut -d"=" -f2 | xargs`
|
||||
OUT=`cd $TERRAFORM_DIR; terraform state show module.turtle_queues_local_$1.aws_sqs_queue.jobs_out | head -1 | cut -d"=" -f2 | xargs`
|
||||
|
||||
cat << EOF > $TURTLE_DIR/.secrets.local-queue
|
||||
AWS_SQS_ANDROID_QUEUE_URL="$ANDROID_NORMAL"
|
||||
AWS_SQS_ANDROID_PRIORITY_QUEUE_URL="$ANDROID_PRIORITY"
|
||||
AWS_SQS_IOS_QUEUE_URL="$IOS_NORMAL"
|
||||
AWS_SQS_IOS_PRIORITY_QUEUE_URL="$IOS_PRIORITY"
|
||||
AWS_SQS_OUT_QUEUE_URL="$OUT"
|
||||
EOF
|
||||
|
||||
@@ -2,6 +2,12 @@
|
||||
|
||||
set -eo pipefail
|
||||
|
||||
if ! command -v jq &> /dev/null
|
||||
then
|
||||
echo "jq is not installed and is required by the release script. Install it through homebrew or your package manager of choice."
|
||||
exit
|
||||
fi
|
||||
|
||||
ROOT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )"/.. && pwd )"
|
||||
|
||||
if [[ -z "$GITHUB_TOKEN" || -z "$CIRCLE_API_USER_TOKEN" ]]; then
|
||||
|
||||
@@ -2,6 +2,12 @@
|
||||
|
||||
set -eo pipefail
|
||||
|
||||
if ! command -v jq &> /dev/null
|
||||
then
|
||||
echo "jq is not installed and is required by the release script. Install it through homebrew or your package manager of choice."
|
||||
exit
|
||||
fi
|
||||
|
||||
ROOT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )"/.. && pwd )"
|
||||
|
||||
$ROOT_DIR/node_modules/.bin/release-it -c $ROOT_DIR/.release-it.beta.json --preRelease=beta
|
||||
@@ -7,7 +7,7 @@ DOWNLOAD_DIR="${EXPO_DOWNLOAD_DIR:-$HOME/Downloads}"
|
||||
mkdir -p $DOWNLOAD_DIR
|
||||
|
||||
if [ ! -f "$ROOT_DIR/.secrets.local-queue" ]; then
|
||||
echo "You need to run \"yarn secrets:init-private [SQS_NAME]\" or create the .secrets.local-queue file yourself with AWS_SQS_ANDROID_QUEUE_URL, AWS_SQS_ANDROID_PRIORITY_QUEUE_URL, AWS_SQS_IOS_QUEUE_URL, AWS_SQS_IOS_PRIORITY_QUEUE_URL and AWS_SQS_OUT_QUEUE_URL variables."
|
||||
echo "You need to run \"yarn secrets:init-private [SQS_NAME]\" or create the .secrets.local-queue file yourself with AWS_SQS_ANDROID_QUEUE_URL, AWS_SQS_ANDROID_PRIORITY_QUEUE_URL, AWS_SQS_IOS_QUEUE_URL, and AWS_SQS_IOS_PRIORITY_QUEUE_URL variables."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
@@ -13,7 +13,6 @@ export AWS_SQS_ANDROID_QUEUE_URL="https://sqs.us-west-2.amazonaws.com/2742511416
|
||||
export AWS_SQS_ANDROID_PRIORITY_QUEUE_URL="https://sqs.us-west-2.amazonaws.com/274251141632/turtle_jobs_local_dominik_android_in_priority.fifo"
|
||||
export AWS_SQS_IOS_QUEUE_URL="https://sqs.us-west-2.amazonaws.com/274251141632/turtle_jobs_local_dominik_ios_in.fifo"
|
||||
export AWS_SQS_IOS_PRIORITY_QUEUE_URL="https://sqs.us-west-2.amazonaws.com/274251141632/turtle_jobs_local_dominik_ios_in_priority.fifo"
|
||||
export AWS_SQS_OUT_QUEUE_URL="https://sqs.us-west-2.amazonaws.com/274251141632/turtle_jobs_local_dominik_out.fifo"
|
||||
export AWS_SQS_REGION="aws.west2"
|
||||
export AWS_S3_BUCKET="exp-shell-app-assets"
|
||||
export AWS_S3_REGION="us-west-1"
|
||||
@@ -26,7 +25,7 @@ export REDIS_CONFIG_URL="noop"
|
||||
export DATADOG_DISABLED="true"
|
||||
export TURTLE_SDK_VERSIONS_SECRET_TOKEN="noop"
|
||||
|
||||
pushd $ROOT_DIR/workingdir/android/${SDK_VERSION:-sdk37} >/dev/null 2>&1
|
||||
pushd $ROOT_DIR/workingdir/android/${SDK_VERSION:-sdk39} >/dev/null 2>&1
|
||||
yarn
|
||||
popd >/dev/null 2>&1
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ export AWS_SQS_ANDROID_QUEUE_URL="https://sqs.us-west-2.amazonaws.com/2742511416
|
||||
export AWS_SQS_ANDROID_PRIORITY_QUEUE_URL="https://sqs.us-west-2.amazonaws.com/274251141632/turtle_jobs_local_dominik_android_in_priority.fifo"
|
||||
export AWS_SQS_IOS_QUEUE_URL="https://sqs.us-west-2.amazonaws.com/274251141632/turtle_jobs_local_dominik_ios_in.fifo"
|
||||
export AWS_SQS_IOS_PRIORITY_QUEUE_URL="https://sqs.us-west-2.amazonaws.com/274251141632/turtle_jobs_local_dominik_ios_in_priority.fifo"
|
||||
export AWS_SQS_OUT_QUEUE_URL="https://sqs.us-west-2.amazonaws.com/274251141632/turtle_jobs_local_dominik_out.fifo"
|
||||
export AWS_SQS_REGION="aws.west2"
|
||||
export AWS_S3_BUCKET="exp-shell-app-assets"
|
||||
export AWS_S3_REGION="us-west-1"
|
||||
|
||||
@@ -9,7 +9,6 @@ export AWS_SQS_IOS_QUEUE_URL="noop"
|
||||
export AWS_SQS_ANDROID_QUEUE_URL="noop"
|
||||
export AWS_SQS_IOS_PRIORITY_QUEUE_URL="noop"
|
||||
export AWS_SQS_ANDROID_PRIORITY_QUEUE_URL="noop"
|
||||
export AWS_SQS_OUT_QUEUE_URL="noop"
|
||||
export AWS_ACCESS_KEY_ID="noop"
|
||||
export AWS_SECRET_ACCESS_KEY="noop"
|
||||
export AWS_SQS_REGION="noop"
|
||||
|
||||
@@ -2,6 +2,12 @@
|
||||
|
||||
set -eo pipefail
|
||||
|
||||
if ! command -v jq &> /dev/null
|
||||
then
|
||||
echo "jq is not installed and is required by the release script. Install it through homebrew or your package manager of choice."
|
||||
exit
|
||||
fi
|
||||
|
||||
DEPLOY_ENDPOINT_URL="https://circleci.com/api/v1.1/project/github/expo/turtle-cli-example/tree/updates-do-not-remove-me"
|
||||
|
||||
curl -X POST \
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
s3://exp-artifacts/android-shell-builder-a8e204610e5712391336b378f47962bbb0c04164.tar.gz
|
||||
@@ -1 +0,0 @@
|
||||
s3://exp-artifacts/android-shell-builder-7ce098f40cc82a2f61fed9f1f515d695a83c55b2.tar.gz
|
||||
@@ -1 +1 @@
|
||||
s3://exp-artifacts/android-shell-builder-960f6b6d4cd61dd67ffc69ab5942be458cc48221.tar.gz
|
||||
s3://exp-artifacts/android-shell-builder-3cf3cf0707925322bcd667d5ba79120136f9afd8.tar.gz
|
||||
@@ -0,0 +1 @@
|
||||
s3://exp-artifacts/android-shell-builder-6cde8e00cd7bdf7a933cd8aad7f8e86981ea68a2.tar.gz
|
||||
@@ -0,0 +1 @@
|
||||
s3://exp-artifacts/android-shell-builder-d292dcd0cc277ab665957244a8b15b9981debd29.tar.gz
|
||||
@@ -0,0 +1 @@
|
||||
s3://exp-artifacts/android-shell-builder-277393d97877f56ba3639ab153c69f109d248436.tar.gz
|
||||
@@ -1 +1 @@
|
||||
s3://exp-artifacts/ios-expo-client-aadfc350d8e4ebf8dce9ce08723bb7e07d563846.tar.gz
|
||||
s3://exp-artifacts/ios-expo-client-dd6b6b4840c7a99725f1ec9a9a3970de91e90b1d.tar.gz
|
||||
@@ -1 +0,0 @@
|
||||
s3://exp-artifacts/ios-shell-builder-sdk-latest-651588ff3445a5d87b3a5b81ec2977c02b22f3a2.tar.gz
|
||||
@@ -1 +0,0 @@
|
||||
s3://exp-artifacts/ios-shell-builder-sdk-latest-0115c48ddac4f63e5215541dff804871ef8c1e03.tar.gz
|
||||
@@ -1 +1 @@
|
||||
s3://exp-artifacts/ios-shell-builder-sdk-latest-661a51216c2241dcfdd930517968cd1d0e0ee237.tar.gz
|
||||
s3://exp-artifacts/ios-shell-builder-sdk-latest-0983415327f49592d81f0bd3dc8c46a3572c19b0.tar.gz
|
||||
@@ -1 +1 @@
|
||||
s3://exp-artifacts/ios-shell-builder-sdk-latest-549b9da7f764c6b75741aad3cd29a53aba809516.tar.gz
|
||||
s3://exp-artifacts/ios-shell-builder-sdk-latest-df407aadb7208fc389564aed400c39263294eb76.tar.gz
|
||||
@@ -0,0 +1 @@
|
||||
s3://exp-artifacts/ios-shell-builder-sdk-latest-ba6ec1974977bebf8fd2db22a1c87f6a0b36ffa5.tar.gz
|
||||
@@ -0,0 +1 @@
|
||||
s3://exp-artifacts/ios-shell-builder-sdk-latest-4a9a038998f6be13e8a2900a95afbb9f96dd023d.tar.gz
|
||||
@@ -0,0 +1 @@
|
||||
s3://exp-artifacts/ios-shell-builder-sdk-latest-dd6b6b4840c7a99725f1ec9a9a3970de91e90b1d.tar.gz
|
||||
@@ -1 +1 @@
|
||||
[{"scope":"https://sqs.us-west-2.amazonaws.com:443","method":"POST","path":"/","body":"Action=ReceiveMessage&MaxNumberOfMessages=1&QueueUrl=https%3A%2F%2Fsqs.us-west-2.amazonaws.com%2F274251141632%2Fturtle_jobs_local_dominik_android_in.fifo&Version=2012-11-05&VisibilityTimeout=30","status":200,"response":"<?xml version=\"1.0\"?><ReceiveMessageResponse xmlns=\"http://queue.amazonaws.com/doc/2012-11-05/\"><ReceiveMessageResult/><ResponseMetadata><RequestId>2f9926f3-9db4-588e-a00a-71f445522996</RequestId></ResponseMetadata></ReceiveMessageResponse>","rawHeaders":["x-amzn-RequestId","2f9926f3-9db4-588e-a00a-71f445522996","Date","Wed, 08 Apr 2020 13:50:35 GMT","Content-Type","text/xml","Content-Length","240"]},{"scope":"https://sqs.us-west-2.amazonaws.com:443","method":"POST","path":"/","body":"Action=ReceiveMessage&MaxNumberOfMessages=1&QueueUrl=https%3A%2F%2Fsqs.us-west-2.amazonaws.com%2F274251141632%2Fturtle_jobs_local_dominik_android_in_priority.fifo&Version=2012-11-05&VisibilityTimeout=30","status":200,"response":"<?xml version=\"1.0\"?><ReceiveMessageResponse xmlns=\"http://queue.amazonaws.com/doc/2012-11-05/\"><ReceiveMessageResult/><ResponseMetadata><RequestId>e42b6179-9846-5d83-9209-367e40aa9c02</RequestId></ResponseMetadata></ReceiveMessageResponse>","rawHeaders":["x-amzn-RequestId","e42b6179-9846-5d83-9209-367e40aa9c02","Date","Wed, 08 Apr 2020 13:50:41 GMT","Content-Type","text/xml","Content-Length","240"]},{"scope":"https://sqs.us-west-2.amazonaws.com:443","method":"POST","path":"/","body":"Action=ReceiveMessage&MaxNumberOfMessages=1&QueueUrl=https%3A%2F%2Fsqs.us-west-2.amazonaws.com%2F274251141632%2Fturtle_jobs_local_dominik_android_in.fifo&Version=2012-11-05&VisibilityTimeout=30","status":200,"response":"<?xml version=\"1.0\"?><ReceiveMessageResponse xmlns=\"http://queue.amazonaws.com/doc/2012-11-05/\"><ReceiveMessageResult><Message><MessageId>093d5a52-932c-4141-958a-4ee8146dadab</MessageId><ReceiptHandle>AQEBq2BDKWkc+YFuyL+qBVQ6RCqne9ndyzDri6BeHTv+HhCU3kOlINEzzqbgrGJXh49AB6lVInGNsI+uc5uMhC61UscDKfFeqgfhDDUftovz6RXwiIj+xRalgP5fPZn94LbY4KmUoCuGCpXu9UlGxOoorYTkLft9AVY6kabCRlXA8SJzzYjUb1s+rtBoWS3Trj/AOIxOy0EqxeEWpBsNKc1yWc9RNbZ8/qABFLMjOzdmr5gaLEDoRJTUZZUEu9TVOxrC54ES+UaCmgVY17Ubmwo884GbHLoyUA+xqSD0Cvify5GCv3xzB+wjB4gGdGUFdOjj</ReceiptHandle><MD5OfBody>f0cb57fa399d2c8e81f8423e93127bdc</MD5OfBody><Body>{"id":"1b148a0a-4a5e-4572-b28a-c31aa018351a","experienceId":"0128bca1-78cf-4cb8-ac00-c07600500254","experienceName":"@dsokal-local/turtle-cli-example","platform":"android","config":{"releaseChannel":"default","buildType":"apk"},"status":"sent-to-queue","sdkVersion":"37.0.0","accountId":"9b473c78-6d0e-4b69-88a8-1c0960287b2f","priority":"normal","credentials":{"keystore":"/u3+7QAAAAIAAAABAAAAAQAscWdyemIydGhiYzFzYjJuaGJjOTBkeGowYmd1dHkyeHBsd3Y0eXcxd2JndT0AAAFxTutg3AAABQEwggT9MA4GCisGAQQBKgIRAQEFAASCBOl0ClAt46XtKkD3OZ4KtYCfGvjcqn+NUvylgValp+eckKR3vpTdmzIJxCCPCPpVWUT9kssEpIWea1oyDbBoh01lSazoofkTt+QIkcnr2sOcWgoyywIH+jNH4CPDfycdjPBsy8IoFqSz5DSS9vWwdO8xw0uw/iBd0MmQUcdJANZb0HbvrOU9R1n8IeNeFZUS0nk8czYplz8e7+lWphAY1vA7oLKwYMvkPrHQSoHaXh7WqOyn3A31eQgTHZGW2vwSa7gUS+X1izYozAY5AaKrxwUYQ56dWMNnuufSGldj8KWIpdps1Bo1bN6bb/eDWiXbEGWXgwJOZxYs9OFiPPw296aBdtWpG3qu11Z+NQmr28ov0uqj9yWBX/+iO8TgyCEBPZHRnlVRvoRIvhXoZ2GYsLMvJsn7GrdPWHzzs1cKkg85Ffh7Tr1+32LkVHqHtuId/2hhC2HeP5Yzgkw45ty29H3SXWpydrDjLwQAtGYDnYxYplRxhSt+FXMsepuytDCHAjJ27Ers9mMOs8LriYznJcup32FSrdvAk8SXdrxIXmZ7q8dgmI0OD6tlS+cNGm9Y0e7kWbFbSXK72YViVWzAL/Rm0ecQPSnOaowpVsvU6vWHCdUxgwWGpJAXPPrVmvoTwzQQ4gjm6d4IvRzfW2pXMGvw+gr/19LicvUkhRLS7qKHaYS1QXss9rxWgE8SdFMJghFnwc2s+V4wDsf44aeX3/0mYegfijXI/2d6htQ4ouojQBmQlK7pKiUGjDepgT3S+G8XBSslBcTv0Vf5sudUcisXH8BQd015Sf5ruE7k0BLObfUWsJSd3b8uRjwAXkjCKILiUOckIuLwIiRNBj39u5HlrXZeuUiK736F9j9xPwVg5RGzW28gaLLSjZhJVogH6LA72aAEMbpt9155dkmzVa8RCk2P4axEknbL5V9Bq0L/7d2ouR+5MSGQf3lvxxd1qI9R+oTUDUwiJG4P/emjrJ+uXV6uBtz6fA9IrT3s8fWbBGcxToUPQCo2xfHp6VKzoV3wxelWq7LXDFHZWTw4WYbuStSBzdxNoRpmv9f+qBRA2hehildALE3Mib9zg5vPIU1byPuWNs6Tgca+Iolnkj/gCu69LbFWbcKsE34b+L840DHGFaYP455elIk3sQmYfQfP0KCqY1xmixPkT3bZQpRjWCGeemTY4hl+kGnVcvKWejOikWlAIoj3ZsjuEIxseIt3IUyLPpRFIqH1thODR+O/h3rutmWUknzvj+LlNJlDxI9heNm7i2toyY8QOvdZiUw0elS7yxL+MPTRfZfOLsV9glHYbivtwku5aRiG15DUPW+00fwOSQXq4x3D07z0QFmnoRl2IrW/113EkkJCtmNbLrWRXQkUaO5Rj4JYWIu9kc7w9GGMS3v1utQ2w8AicBqbYPMy7QnqSfok/FZVntBB/xQDoecXfMlA/G1x4WYe2/gaXjydSNSAx/ta7zbgVGU0SxNVbBs6vyuvlMqNpnHlS3wmlSkl0qGNqHuRvl1qfc5gJJ3eCTkZdqSMocVjHkutc0gja2gStLxdEGTxh12CvmSmn0vve5qWhuHi1xZxSLJutImspnXuLDKSu5tWPZsi38CAsxAR3sDZ6xe+0EuiJwK8w3R/r6Cz7rZRwYm/mg4x0BvDhkWeuvYJEtAZKCKLlumodK2gl7AAAAABAAVYLjUwOQAAA2EwggNdMIICRaADAgECAgQrux+aMA0GCSqGSIb3DQEBCwUAMF8xCzAJBgNVBAYTAlVTMQkwBwYDVQQIEwAxCTAHBgNVBAcTADEJMAcGA1UEChMAMQkwBwYDVQQLEwAxJDAiBgNVBAMTG2NvbS5leHBvLnR1cnRsZS5jbGkuZXhhbXBsZTAeFw0yMDA0MDYwOTU2MjBaFw00NzA4MjMwOTU2MLine truncated
|
||||
[{"scope":"https://sqs.us-west-2.amazonaws.com:443","method":"POST","path":"/","body":"Action=ReceiveMessage&MaxNumberOfMessages=1&QueueUrl=https%3A%2F%2Fsqs.us-west-2.amazonaws.com%2F274251141632%2Fturtle_jobs_local_dominik_android_in.fifo&Version=2012-11-05&VisibilityTimeout=30","status":200,"response":"<?xml version=\"1.0\"?><ReceiveMessageResponse xmlns=\"http://queue.amazonaws.com/doc/2012-11-05/\"><ReceiveMessageResult/><ResponseMetadata><RequestId>8de5bfc7-bc8e-5ee3-9e04-e787ff880c47</RequestId></ResponseMetadata></ReceiveMessageResponse>","rawHeaders":["x-amzn-RequestId","8de5bfc7-bc8e-5ee3-9e04-e787ff880c47","Date","Tue, 01 Dec 2020 13:10:35 GMT","Content-Type","text/xml","Content-Length","240"]},{"scope":"https://sqs.us-west-2.amazonaws.com:443","method":"POST","path":"/","body":"Action=ReceiveMessage&MaxNumberOfMessages=1&QueueUrl=https%3A%2F%2Fsqs.us-west-2.amazonaws.com%2F274251141632%2Fturtle_jobs_local_dominik_android_in_priority.fifo&Version=2012-11-05&VisibilityTimeout=30","status":200,"response":"<?xml version=\"1.0\"?><ReceiveMessageResponse xmlns=\"http://queue.amazonaws.com/doc/2012-11-05/\"><ReceiveMessageResult/><ResponseMetadata><RequestId>0bb998bb-3156-5159-b16f-c0fb725a5d65</RequestId></ResponseMetadata></ReceiveMessageResponse>","rawHeaders":["x-amzn-RequestId","0bb998bb-3156-5159-b16f-c0fb725a5d65","Date","Tue, 01 Dec 2020 13:10:41 GMT","Content-Type","text/xml","Content-Length","240"]},{"scope":"https://sqs.us-west-2.amazonaws.com:443","method":"POST","path":"/","body":"Action=ReceiveMessage&MaxNumberOfMessages=1&QueueUrl=https%3A%2F%2Fsqs.us-west-2.amazonaws.com%2F274251141632%2Fturtle_jobs_local_dominik_android_in.fifo&Version=2012-11-05&VisibilityTimeout=30","status":200,"response":"<?xml version=\"1.0\"?><ReceiveMessageResponse xmlns=\"http://queue.amazonaws.com/doc/2012-11-05/\"><ReceiveMessageResult/><ResponseMetadata><RequestId>feef69fc-27a3-5af5-83af-31bc64b1b9d7</RequestId></ResponseMetadata></ReceiveMessageResponse>","rawHeaders":["x-amzn-RequestId","feef69fc-27a3-5af5-83af-31bc64b1b9d7","Date","Tue, 01 Dec 2020 13:10:47 GMT","Content-Type","text/xml","Content-Length","240"]},{"scope":"https://sqs.us-west-2.amazonaws.com:443","method":"POST","path":"/","body":"Action=ReceiveMessage&MaxNumberOfMessages=1&QueueUrl=https%3A%2F%2Fsqs.us-west-2.amazonaws.com%2F274251141632%2Fturtle_jobs_local_dominik_android_in_priority.fifo&Version=2012-11-05&VisibilityTimeout=30","status":200,"response":"<?xml version=\"1.0\"?><ReceiveMessageResponse xmlns=\"http://queue.amazonaws.com/doc/2012-11-05/\"><ReceiveMessageResult><Message><MessageId>f39aaec9-47e0-4d02-bdb7-cf467fe2ea19</MessageId><ReceiptHandle>AQEBBI8nzAywIPCH57h1Iptt39uq60E5dzlj+Er50gtGqH+esoJ6iFdxoPDxvtD0xfT0qlyKSqlmoBDkQwh36tqsfqdT5FcFF8ejmPMRVIE8hexofzEbacxxw+3cBfgra2EFAik49TJbMT5pwxyaf42y+qwyWKOZ5N62VzRgGtnpj5ilNw1dt0lUzCdsfhCu7sne+dfJ8oMpbuy6T+ttoEYUS6VVKmv8f1BzuujHoKMy0kJip8r3zFoaigWE2p1bpFby9b9oBFTFT1ycD618YTu+Un5S9Q7Q6/nqJ7RPM2BfK2iPxBMk+UWmGfJ/otX11Zc0p8qx/MyKuYY/480Zta//xQ==</ReceiptHandle><MD5OfBody>1942fb5be12b59fc93b8052cf960f7d8</MD5OfBody><Body>{"id":"686530e4-c025-4a01-b84b-041e6d4f7fb3","experienceId":"4d928c1c-5be9-4380-8fd5-48a6a4805839","experienceName":"@testtest2/sdk-39-test","platform":"android","config":{"releaseChannel":"default","buildType":"apk"},"status":"sent-to-queue","sdkVersion":"39.0.0","accountId":"1ecd48ee-e393-4f6b-9f77-311f2e0d2b39","priority":"high","credentials":{"keystore":"/u3+7QAAAAIAAAABAAAAAQAgcWhybGMzcjB6eG4wbWk5enpnc3Rtemt0ZGd2emRhPT0AAAF2HlYoCQAABQEwggT9MA4GCisGAQQBKgIRAQEFAASCBOmzIBnRnHGBLgRUOJ1GGYiLjktnTeq/GXR/h4KZSSQ67PoiJqJnhrHLvj35Sr1swSL78q4j52IRV/a1mFbqQT4JFFy1ZdH2XgKUptY+kVc+ZrBI/awC8akIeYofWmKTNcS0Bc9IDT1VwIzwxxRrlnK/36Gh2m1vRuwVUdBChHjB4SHgC91F8wB0opY+RmiRhZqQevd8PBvZyqKenR5IWgZDXhsUYvvby4U4zMzvEgV3zNv7YGYM1Bb2ehJ8HdsW6O/58zFL3ufJ3+H2VonLxSn9YayninHoqkyMP40/mmnaFPz5jNItDkCQIeuudgQc8mrZlv4X0lM2ZjT4ZJDLdN4FCXV5iYDe4hopF8iF+DZmhMaZ1xFJ57boEaZ+WUcwsTkKOawusOGqW2JsvZbDMehOYLc8aqM2t28PrdYf3c7zspJALi6+tar2mwN66TX/AteeJRAk3r7HAiYPyle5gOffGgpmy0klLb5MEStkLQNNNsVMLJ7XSYrewzI6jXyAbMVc6PzbiVnCRPtoSy0TMUD7qQWMUGdePL1jxDlwhOVS3jgwOwHIFhUaLBbQOPROm1bF4hY3p25+LHFu954aDsYC3JlT6k9EOKIacKbJThq14r2Lb4EekfQlYTO/yst6ncVJcawWHYS1f93xWUlIjcpFnqJrLq2QVQfhafPWJmTS3K0lcYgFpfxvoU8RVn+ySaLOiy9J86l5YPmtpH/nFqr6BkqROy6dXps5x+D9vi3jVfYlYHnmqHxq7vyX5X+IMIMLvJ28SviuMKWb8Ikqmm7FSzlewJfF4xy/NLgQrQY6VxUahy92PZf3uR02ENolnj5nPSnh/B1FnIfrG46xFqlPg8imzen7hjKk1yXrdUjOT4WcjLdD/K/heVVHKUnsn3QCBt6eOgB8MpG9XzGRD+PAwloklASQx8iVSD67EauU2y1yayf4lNaQzdfnF3iFXSF25BU9XLJIRTtdRsTwmLRuLTmQcctLYD8hQCDQ2/njZtTkdY/AJeVlm44CdDaTLJpP8mVVscAOgH/S0DPi/oGGie8AYeKr8ZmBO1lQSN6l2LBJcAq2fh6AaUF5By21lT9FfR9KQd5WM87iUZk41nBIHQXcQSSt7H+vETEfm/8T9tOOwY5dMyf5QAYNCZuBoIFNCr0MOxe/Dcrsvd46KVbftR4HsJsQl53MnZoh4eLine truncated
|
||||
@@ -1 +1 @@
|
||||
[{"scope":"https://sqs.us-west-2.amazonaws.com:443","method":"POST","path":"/","body":"Action=ReceiveMessage&MaxNumberOfMessages=1&QueueUrl=https%3A%2F%2Fsqs.us-west-2.amazonaws.com%2F274251141632%2Fturtle_jobs_local_dominik_ios_in.fifo&Version=2012-11-05&VisibilityTimeout=30","status":200,"response":"<?xml version=\"1.0\"?><ReceiveMessageResponse xmlns=\"http://queue.amazonaws.com/doc/2012-11-05/\"><ReceiveMessageResult/><ResponseMetadata><RequestId>8bea4e96-2dc5-572c-9898-1991f406b911</RequestId></ResponseMetadata></ReceiveMessageResponse>","rawHeaders":["x-amzn-RequestId","8bea4e96-2dc5-572c-9898-1991f406b911","Date","Wed, 08 Apr 2020 13:54:16 GMT","Content-Type","text/xml","Content-Length","240"]},{"scope":"https://sqs.us-west-2.amazonaws.com:443","method":"POST","path":"/","body":"Action=ReceiveMessage&MaxNumberOfMessages=1&QueueUrl=https%3A%2F%2Fsqs.us-west-2.amazonaws.com%2F274251141632%2Fturtle_jobs_local_dominik_ios_in_priority.fifo&Version=2012-11-05&VisibilityTimeout=30","status":200,"response":"<?xml version=\"1.0\"?><ReceiveMessageResponse xmlns=\"http://queue.amazonaws.com/doc/2012-11-05/\"><ReceiveMessageResult/><ResponseMetadata><RequestId>a995b5e8-7f64-5567-baf9-e2efa6add8fe</RequestId></ResponseMetadata></ReceiveMessageResponse>","rawHeaders":["x-amzn-RequestId","a995b5e8-7f64-5567-baf9-e2efa6add8fe","Date","Wed, 08 Apr 2020 13:54:22 GMT","Content-Type","text/xml","Content-Length","240"]},{"scope":"https://sqs.us-west-2.amazonaws.com:443","method":"POST","path":"/","body":"Action=ReceiveMessage&MaxNumberOfMessages=1&QueueUrl=https%3A%2F%2Fsqs.us-west-2.amazonaws.com%2F274251141632%2Fturtle_jobs_local_dominik_ios_in.fifo&Version=2012-11-05&VisibilityTimeout=30","status":200,"response":"<?xml version=\"1.0\"?><ReceiveMessageResponse xmlns=\"http://queue.amazonaws.com/doc/2012-11-05/\"><ReceiveMessageResult/><ResponseMetadata><RequestId>8871c5eb-cb40-5a57-a611-92ec128fccd4</RequestId></ResponseMetadata></ReceiveMessageResponse>","rawHeaders":["x-amzn-RequestId","8871c5eb-cb40-5a57-a611-92ec128fccd4","Date","Wed, 08 Apr 2020 13:54:28 GMT","Content-Type","text/xml","Content-Length","240"]},{"scope":"https://sqs.us-west-2.amazonaws.com:443","method":"POST","path":"/","body":"Action=ReceiveMessage&MaxNumberOfMessages=1&QueueUrl=https%3A%2F%2Fsqs.us-west-2.amazonaws.com%2F274251141632%2Fturtle_jobs_local_dominik_ios_in_priority.fifo&Version=2012-11-05&VisibilityTimeout=30","status":200,"response":"<?xml version=\"1.0\"?><ReceiveMessageResponse xmlns=\"http://queue.amazonaws.com/doc/2012-11-05/\"><ReceiveMessageResult/><ResponseMetadata><RequestId>5c49f623-617a-531c-bbf8-a124b8af2bfd</RequestId></ResponseMetadata></ReceiveMessageResponse>","rawHeaders":["x-amzn-RequestId","5c49f623-617a-531c-bbf8-a124b8af2bfd","Date","Wed, 08 Apr 2020 13:54:34 GMT","Content-Type","text/xml","Content-Length","240"]},{"scope":"https://sqs.us-west-2.amazonaws.com:443","method":"POST","path":"/","body":"Action=ReceiveMessage&MaxNumberOfMessages=1&QueueUrl=https%3A%2F%2Fsqs.us-west-2.amazonaws.com%2F274251141632%2Fturtle_jobs_local_dominik_ios_in.fifo&Version=2012-11-05&VisibilityTimeout=30","status":200,"response":"<?xml version=\"1.0\"?><ReceiveMessageResponse xmlns=\"http://queue.amazonaws.com/doc/2012-11-05/\"><ReceiveMessageResult/><ResponseMetadata><RequestId>68b706ef-2dce-505b-a0d6-51e8f412e6f9</RequestId></ResponseMetadata></ReceiveMessageResponse>","rawHeaders":["x-amzn-RequestId","68b706ef-2dce-505b-a0d6-51e8f412e6f9","Date","Wed, 08 Apr 2020 13:54:41 GMT","Content-Type","text/xml","Content-Length","240"]},{"scope":"https://sqs.us-west-2.amazonaws.com:443","method":"POST","path":"/","body":"Action=ReceiveMessage&MaxNumberOfMessages=1&QueueUrl=https%3A%2F%2Fsqs.us-west-2.amazonaws.com%2F274251141632%2Fturtle_jobs_local_dominik_ios_in_priority.fifo&Version=2012-11-05&VisibilityTimeout=30","status":200,"response":"<?xml version=\"1.0\"?><ReceiveMessageResponse xmlns=\"http://queue.amazonaws.com/doc/2012-11-05/\"><ReceiveMessageResult/><ResponseMetadata><RequestId>c1e6a3e3-8d97-57cd-884c-f4e454757fcd</RequestId></ResponseMetadata></ReceiveMessageResponse>","rawHeaders":["x-amzn-RequestId","c1e6a3e3-8d97-57cd-884c-f4e454757fcd","Date","Wed, 08 Apr 2020 13:54:47 GMT","Content-Type","text/xml","Content-Length","240"]},{"scope":"https://sqs.us-west-2.amazonaws.com:443","method":"POST","path":"/","body":"Action=ReceiveMessage&MaxNumberOfMessages=1&QueueUrl=https%3A%2F%2Fsqs.us-west-2.amazonaws.com%2F274251141632%2Fturtle_jobs_local_dominik_ios_in.fifo&Version=2012-11-05&VisibilityTimeout=30","status":200,"response":"<?xml version=\"1.0\"?><ReceiveMessageResponse xmlns=\"http://queue.amazonaws.com/doc/2012-11-05/\"><ReceiveMessageResult/><ResponseMetadata><RequestId>a68dea4b-2de4-5000-b718-bd4cf40ca8fa</RequestId></ResponseMetadata></ReceiveMessageResponse>","rawHeaders":["x-amzn-RequestId","a68dea4b-2de4-5000-b718-bd4cf40ca8fa","Date","Wed, 08 Apr 2020 13:54:53 GMT","Content-Type","text/xml","Content-Length","240"]},{Line truncated
|
||||
[{"scope":"https://sqs.us-west-2.amazonaws.com:443","method":"POST","path":"/","body":"Action=ReceiveMessage&MaxNumberOfMessages=1&QueueUrl=https%3A%2F%2Fsqs.us-west-2.amazonaws.com%2F274251141632%2Fturtle_jobs_local_dominik_ios_in.fifo&Version=2012-11-05&VisibilityTimeout=30","status":200,"response":"<?xml version=\"1.0\"?><ReceiveMessageResponse xmlns=\"http://queue.amazonaws.com/doc/2012-11-05/\"><ReceiveMessageResult><Message><MessageId>99e0e5ee-90ef-494e-a275-3c1d55b58d9c</MessageId><ReceiptHandle>AQEB73473140rmgRe58W4T81ev8RJFQ0PvS6vOcHcSVdAV9D+bmP66k8qIqGuc0LJqR4bE9GLWG+QnM7N8rRW1tjIB74Gt9Co3PD7KFwrdgjZ3eys3fjK9nMfOj7BI0DMIP2xWi3YC90300dr/tTgJhVxfDh+Y0nYWleN9qEmttJoRqlJdSMgokqBR5VBFbhAmoNfsbSQnLzwU7fe/g2gjarImBRaCYpNWHYmS9CLozP5RBGKCddzQLvaNxDwORZXzHbYPZha5eFIDfXa3ncBtK2XdbOiquCmEB1tMt9ziIEOObSt/Vu7CD+ANhRqdqEFL/x</ReceiptHandle><MD5OfBody>ebe1f299d094649b2f3857bbd6f380ec</MD5OfBody><Body>{"id":"883f68d1-398a-4327-83da-6de1f288456d","experienceId":"2945b084-8989-4a02-b781-2a620034a172","experienceName":"@dsokal/turtle-cli-example","platform":"ios","config":{"releaseChannel":"default","usesNonExemptEncryption":false,"buildType":"archive","bundleIdentifier":"com.expo.turtle.cli.example"},"status":"sent-to-queue","sdkVersion":"39.0.0","accountId":"07eb1118-80a9-4769-a9b5-9f429cab29e2","priority":"normal","credentials":{"apnsKeyId":"54G5NV49UY","apnsKeyP8":"-----BEGIN PRIVATE KEY-----\\nMIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQgBtkLIodAlsdUmV1L\\nxirn9Erof29278yzTaR/NNlvF0igCgYIKoZIzj0DAQehRANCAAS5jlfFX2KmTFi/\\ng+aohp77kCfYRZNYYmn4cgn4dKkXu5YOGbTJlzW6384xDpX6uWcXXytKD9cO00uN\\n8v9j4MT6\\n-----END PRIVATE KEY-----","teamId":"QL76XYH73P","teamName":"Alicja Warchał (Individual)","certP12":"MIIMDAIBAzCCC9YGCSqGSIb3DQEHAaCCC8cEggvDMIILvzCCBl8GCSqGSIb3DQEHBqCCBlAwggZMAgEAMIIGRQYJKoZIhvcNAQcBMBwGCiqGSIb3DQEMAQYwDgQIyCGcdprP/WkCAggAgIIGGFqAu4HyWNRsBd7y5oylLA5K4/qtIggDVitAITLQ8lpp8XdzuFkYUpzvIY65WPuS3s/JNX1HcFPbldFqH2p10j5E32WuH1sKxht8OVVmjdMRWqxvU/EUZDQHwX+Os4Mx1h8zQub7JYjQkBpb+TbrZwG/qONyIROJXOPTmF6MyZaQeQ4JSgXxEaDLRJ4z94vu2KXGFIVhBKvs1w78I61OzDKsfiG4kWi0p903wvSfsH4IisigSry9UVGM+jsKU5ZUemgrMWZt23dxOx3kHTLTYH+uGAnu6hwzd3vg1s4wtU1hyDxlDS+2pqPF2wdVA9vnPgFagd0oarMuAbYvgweCjplFgh3biDr+jmson9WPHEaaCe5j1bFl9DFfRz4YyZd0QhUEmjR0idWruEeDTvg+DZ1ULTM3g16xr96E9ku8FOzrcnU3vYsNzD31ltQT2K0MoHGTFNjeX+foM7dgvcDaOgGSwm88+aVCK8/8Vrk0wEYbomlJxh+//2dchpHO0EDGyYaF/f06fv6ZfqOKp+/XMbvDP/iLVN/dOthwuM6aRHIZfQ26V5ddscXCQZKGNpCdJWJNSFdhPKBU3QaLE5mn3IKL+wtxwPPORtJVe5hCh0jAsKY6X+bAp+9M0lTqT5TjSPT1BtoLDVQBLkUiYdBYtVBs132MPFnhxPUZeth+YttFLZpg2eT8C74d83LR9azr6jod7VyTcUjDYNgFsyGdmnyeZ8WLnHWEP+LJJbheUlcj6MhuG7cP9doEWzHk3lwGCn9aRW9J/r7FO+nge88CMZ8UCfGJEYkvqxQArT7a06dEzE/YtMsWS5VQ38h6ByryexO22PZx2Ztcd01fcCf7SNmB5m6GfxsABmDdRukUADsid1Tx+sTT52T7CJlgvOjueHHitIjv7XOkydZ81wWmbSm/3PXcjpzFQk48JGRYErFfcKZQRKtDiYsNMENGhIiqEds2BvwENKuHpsa5EMogj5Eoq9VEdfyKytbEdqe/ri/DvG6IyuS1a7e5UKSP8IXV3/N5ekL9p6xJmXYkqQT9v/ByhIS92Kt3z1ySmWUNWl1FOqDC2Ss00weAE9UFS9DQYLk9hwrqgSCjH5ejIf+AAUb0Fg/ujnsEbC9FVbKbUKq1L0uk1w1q/j17yMAFCyHYqnrJD2xqUjm2ijhwZ1ln1Xrin6l6pcLy6nGYRw1AC+JNNlIQvNPanBI0s/ADF1q2SjLgFa2ohMsV1yEtkPVHokgvrFW8lLAiSX0FXauRbnx6U6eby5j8Mz3MGYKDurqYYmoYQW6ip6Dekg9aE81+PC12SrPfsTiYkOvThiNfyvzirMR7ZsZmTiMWIJKFow0g5ut13eEM+5WnbnK94xVUf9uRP1cgbN9UC0HgyzDQGT0gX84HCT9mIKj0PmofuXzuaMme+3BRTRIkWRZdErPyEJOEzmO0EY7cPFLh3uGa2J7tFSHGgv49ADSayagiYs5bIn1YpoOMINfawYINtzljten2qpfphJXBBcU5dP9T7pYaeLv0xXh0Xye895Qr7So/OuWqSlCaxigRCFP7rXgEISABgDkn+Hp2Dq9ebAzNfRPhx5n63crDyyRPtNb26H29u6hfIUbj2i1uBcbASYXZAxSoM20wjzPm+RGv9tsalXGZhJYEHJqBp/S9hhKxTpKjvIr74gZpbSQxfM0L5K3lmKJ6SNjIngWWLUKN87TtAqanwJgP+GJ+66VfxQdQdxI/bGg94GY1qjJqXyU09J9jMXfA9+oD53w6Fj7Tjry3puqDGB9mEVwKEpK8F6CYs3PKYwbG3Al6RNVpaE+qIPYdGnBTDyd7BHvFDswFYUNkRWtbmawsae9S0sICIc926QK4fQHa304lX0AN82juSVUBeCdEI4sOopm2QMNFkczfVEV7GjFCXi58XRCntfFiwNUI/asC4wjzIMmga3qmBtY5aWrW0lIOge48Gb3f9RDOMCEY/hcZouuLkiRQ2E27hspaHd0+op0zpc/KhV0jSrzCzRnol3YG1kFt8c39F345lpVAPiHMoinYSakpAnn0zRjNXzIC+zfdpgBzV0x1Fv5C9vanqCUUEp/9OzCCBVgGCSqGSIb3DQEHAaCCBUkEggVFMIIFQTCCBT0GCyqGSIb3DQEMCgECoIIE7jCCBOowHAYKKoZIhvcNAQwBAzAOBAgX37um07SbJwICCAAEggTIcFrbpfb6ISarEIHkvxM7JcLn+UtU1aolZ++BuMPrKZl9Bnau1bLjg0BJLxMfycvJ9TsjvgH/b8y1BjF7uesn4XxbAlc8HoyA+8/t5emjiEicISNwcVTMHWPPY0TV88xPeAKHAG5HugXIsOaVjkrI0l36M1e9+J6MfF6sG4+phhK/YKmPJtwRwXsrWDLdt4ScG/uQi+tBG+Be02kVHjg9rUrBokmuPGF9/q/2k/f4f3VkDlFGNugubc6moPS73X1wnWdOD3ob32Hx4lhcAUngmtqDF8Uczsk5YZz0vjFd1Go9uWNbI+hjj8Pjv/iU1Tcov6VcCPxXfYYRKYJlRrurdA+ZLwGXBXpU0thEv1RMj9RTljPecU783qvDs6kj3AEh5K6RBoPauOGDkZZzxvz8jbcZb9aY/GZddHESnAA22yEcYbMeyQb0d1HYiOC6Wy6RGsy7PqDRg/8gNHna4BYUMgJD6EfCp5gt6hyjA7fVi/HI9S4GmfUlxR4IBe1t8BvmKKL8WgzydRupn3/hXpEcg3P2rbLH8L4OqnEV4Dpgp5ic+a6HjqVgK/EihO6gPA7HftGPJ6Egs62Line truncated
|
||||
@@ -17,8 +17,7 @@ jest.mock('../builders/utils/uploader.ts', () => ({
|
||||
async function performSmokeTest(recording: string) {
|
||||
jest.setTimeout(60 * 60 * 1000);
|
||||
|
||||
// @ts-ignore
|
||||
const fixtureExists = await fs.exists(recording);
|
||||
const fixtureExists = await fs.pathExists(recording);
|
||||
if (fixtureExists) {
|
||||
logger.info('Fixture exists, test will use existing one.');
|
||||
|
||||
|
||||
@@ -136,7 +136,7 @@ function _addEmptyMetrics(metrics: IMetric[]) {
|
||||
acc.add(i.MetricName);
|
||||
}
|
||||
return acc;
|
||||
}, new Set());
|
||||
}, new Set<string>());
|
||||
|
||||
const nonEmptyMetrics = _.omitBy(state.registeredMetrics, ({ addEmpty }) => !addEmpty);
|
||||
const toAdd = _.difference(Object.keys(nonEmptyMetrics), Array.from(seenMetrics));
|
||||
|
||||
@@ -19,10 +19,10 @@ export default function calculateStatistics(metrics: IMetric[]) {
|
||||
});
|
||||
}
|
||||
|
||||
result.StatisticValues.Maximum = Math.max(result.StatisticValues.Maximum, i.Value);
|
||||
result.StatisticValues.Minimum = Math.min(result.StatisticValues.Minimum, i.Value);
|
||||
result.StatisticValues.SampleCount += 1;
|
||||
result.StatisticValues.Sum += i.Value;
|
||||
result!.StatisticValues.Maximum = Math.max(result!.StatisticValues.Maximum, i.Value);
|
||||
result!.StatisticValues.Minimum = Math.min(result!.StatisticValues.Minimum, i.Value);
|
||||
result!.StatisticValues.SampleCount += 1;
|
||||
result!.StatisticValues.Sum += i.Value;
|
||||
return result;
|
||||
}, null);
|
||||
return [reduced];
|
||||
|
||||
+1
-18
@@ -2,9 +2,8 @@ import retry from 'async-retry';
|
||||
import AWS from 'aws-sdk';
|
||||
import _ from 'lodash';
|
||||
|
||||
import { OUTPUT_QUEUE_URL, QUEUE_URL } from 'turtle/aws/utils';
|
||||
import { QUEUE_URL } from 'turtle/aws/utils';
|
||||
import config from 'turtle/config';
|
||||
import { JOB_UPDATE_TYPE } from 'turtle/constants/build';
|
||||
import logger from 'turtle/logger';
|
||||
import { getCurrentJobId } from 'turtle/turtleContext';
|
||||
|
||||
@@ -73,19 +72,3 @@ export function changeMessageVisibilityRecurring(priority: string, receiptHandle
|
||||
}
|
||||
}, (VISIBILITY_TIMEOUT_SEC * 1000) / 3);
|
||||
}
|
||||
|
||||
export async function sendMessage(jobId: string, status: JOB_UPDATE_TYPE, data = {}) {
|
||||
const params = {
|
||||
MessageBody: JSON.stringify({ jobId, status, ...data }),
|
||||
QueueUrl: OUTPUT_QUEUE_URL(),
|
||||
DelaySeconds: 0,
|
||||
MessageGroupId: jobId,
|
||||
};
|
||||
try {
|
||||
return await retry(async () => {
|
||||
return await sqs.sendMessage(params).promise();
|
||||
});
|
||||
} catch (err) {
|
||||
logger.error({ err }, `Error sending SQS message: ${JSON.stringify(params)}}`);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
import config from 'turtle/config';
|
||||
|
||||
export const QUEUE_URL = (priority: string) => (config.sqs.queues as any)[priority][config.platform];
|
||||
export const OUTPUT_QUEUE_URL = () => config.sqs.queues.out;
|
||||
@@ -29,6 +29,11 @@ export default (program: any, setCommonCommandOptions: any) => {
|
||||
/^(debug|release)$/i,
|
||||
'release',
|
||||
)
|
||||
.option(
|
||||
'--gradle-args <gradle-args>',
|
||||
'optional arguments passed to gradle, make sure to surround them with double quotes'
|
||||
+ ' (e.g.: --gradle-args "--stacktrace --debug")',
|
||||
)
|
||||
.description(
|
||||
'Build a standalone APK or App Bundle for your project, either signed and ready for submission to'
|
||||
+ ' the Google Play Store or in debug mode.',
|
||||
|
||||
@@ -15,7 +15,12 @@ function setCommonCommandOptions(cmd: any) {
|
||||
.option('-o --output <output-file-path>', 'output file path')
|
||||
.option(
|
||||
'--public-url <url>',
|
||||
'the URL of an externally hosted manifest (for self-hosted apps), only HTTPS URLs are supported!',
|
||||
'the URL of an externally hosted manifest (for self-hosted apps), only HTTPS URLs are supported unless '
|
||||
+ '--allow-non-https-public-url is also set!',
|
||||
)
|
||||
.option(
|
||||
'--allow-non-https-public-url',
|
||||
'allows non-HTTPS URLs in --public-url; this should only be used when building an unpublished application',
|
||||
)
|
||||
.option(
|
||||
'--release-channel <channel-name>',
|
||||
|
||||
+31
-14
@@ -1,19 +1,27 @@
|
||||
import path from 'path';
|
||||
|
||||
import { LoggerDetach, ModuleVersion } from '@expo/xdl';
|
||||
import program, { Command } from 'commander';
|
||||
import fs from 'fs-extra';
|
||||
import { LoggerDetach, ModuleVersion } from '../xdl';
|
||||
|
||||
import * as commands from 'turtle/bin/commands';
|
||||
import logger from 'turtle/logger';
|
||||
|
||||
const { name, version } = JSON.parse(fs.readFileSync(path.join(__dirname, '../../package.json'), 'utf-8'));
|
||||
const { name, version } = JSON.parse(
|
||||
fs.readFileSync(path.join(__dirname, '../../package.json'), 'utf-8'),
|
||||
);
|
||||
|
||||
const ModuleVersionChecker = ModuleVersion.createModuleVersionChecker(name, version);
|
||||
const ModuleVersionChecker = ModuleVersion.createModuleVersionChecker(
|
||||
name,
|
||||
version,
|
||||
);
|
||||
|
||||
// @ts-ignore: Type mismatch
|
||||
LoggerDetach.configure(logger);
|
||||
|
||||
Command.prototype.asyncAction = function asyncAction(asyncFn: (...asyncFnArgs: any[]) => void): Command {
|
||||
Command.prototype.asyncAction = function asyncAction(
|
||||
asyncFn: (...asyncFnArgs: any[]) => void,
|
||||
): Command {
|
||||
return this.action(async (...args) => {
|
||||
try {
|
||||
await checkForUpdateAsync();
|
||||
@@ -33,19 +41,24 @@ export function run(programName: string) {
|
||||
|
||||
async function runAsync(programName: string) {
|
||||
program.version(version);
|
||||
Object.values(commands).forEach((command) => registerCommand(program, command));
|
||||
Object.values(commands).forEach((command) =>
|
||||
registerCommand(program, command),
|
||||
);
|
||||
program.parse(process.argv);
|
||||
|
||||
const subCommand = process.argv[2];
|
||||
if (subCommand) {
|
||||
const commandNames = program.commands.reduce((acc: string[], command: any) => {
|
||||
acc.push(command._name);
|
||||
const alias = command._alias;
|
||||
if (alias) {
|
||||
acc.push(alias);
|
||||
}
|
||||
return acc;
|
||||
}, []);
|
||||
const commandNames = program.commands.reduce(
|
||||
(acc: string[], command: any) => {
|
||||
acc.push(command._name);
|
||||
const alias = command._alias;
|
||||
if (alias) {
|
||||
acc.push(alias);
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
[],
|
||||
);
|
||||
if (!commandNames.includes(subCommand)) {
|
||||
logger.error(
|
||||
`"${subCommand}" is not an ${programName} command. See "${programName} --help" for the full list of commands.`,
|
||||
@@ -59,7 +72,11 @@ async function runAsync(programName: string) {
|
||||
const registerCommand = (prog: any, command: any) => command(prog);
|
||||
|
||||
async function checkForUpdateAsync() {
|
||||
const { updateIsAvailable, current, latest } = await ModuleVersionChecker.checkAsync();
|
||||
const {
|
||||
updateIsAvailable,
|
||||
current,
|
||||
latest,
|
||||
} = await ModuleVersionChecker.checkAsync();
|
||||
if (updateIsAvailable) {
|
||||
logger.warn(
|
||||
`There is a new version of ${name} available (${latest}).
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import path from 'path';
|
||||
import util from 'util';
|
||||
|
||||
import { ExponentTools } from '@expo/xdl';
|
||||
import fs from 'fs-extra';
|
||||
import _which from 'which';
|
||||
import { ExponentTools } from '../../../xdl';
|
||||
|
||||
import ensureAndroidSDKIsPresent from 'turtle/bin/setup/android/sdk';
|
||||
import { checkSystem, ensureShellAppIsPresent } from 'turtle/bin/setup/utils/common';
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
|
||||
import { ExponentTools } from '@expo/xdl';
|
||||
import decompress from 'decompress';
|
||||
import fs from 'fs-extra';
|
||||
import { ExponentTools } from '../../../xdl';
|
||||
|
||||
import * as utils from 'turtle/bin/setup/utils/common';
|
||||
import download from 'turtle/bin/setup/utils/downloader';
|
||||
@@ -67,6 +67,7 @@ function _createEnvVars(androidSdkDir: string) {
|
||||
return {
|
||||
ANDROID_HOME: androidSdkDir,
|
||||
ANDROID_SDK: androidSdkDir,
|
||||
ANDROID_NDK_HOME: path.join(androidSdkDir, 'ndk/17.2.4988734'),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import trim from 'lodash/trim';
|
||||
import path from 'path';
|
||||
import semver from 'semver';
|
||||
|
||||
import { ExponentTools } from '@expo/xdl';
|
||||
import { ExponentTools } from '../../xdl';
|
||||
|
||||
import { checkSystem, ensureShellAppIsPresent } from 'turtle/bin/setup/utils/common';
|
||||
import { IToolDefinition } from 'turtle/bin/setup/utils/toolsDetector';
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import path from 'path';
|
||||
|
||||
import { ExponentTools } from '@expo/xdl';
|
||||
import fs from 'fs-extra';
|
||||
import tar from 'tar';
|
||||
import { ExponentTools } from '../../../xdl';
|
||||
|
||||
import download from 'turtle/bin/setup/utils/downloader';
|
||||
import { ensureToolsAreInstalled, IToolDefinition } from 'turtle/bin/setup/utils/toolsDetector';
|
||||
@@ -64,7 +64,7 @@ export function formatArtifactDownloadPath(uri: string) {
|
||||
|
||||
async function _readShellAppTarballS3Uri(sdkVersion: string, formatters: IShellAppFormaters) {
|
||||
const sdkMajorVersion = ExponentTools.parseSdkMajorVersion(sdkVersion);
|
||||
const filePath = formatters.formatShellAppTarballUriPath(sdkMajorVersion);
|
||||
const filePath = formatters.formatShellAppTarballUriPath(String(sdkMajorVersion));
|
||||
const data = await fs.readFile(filePath, 'utf8');
|
||||
return data.trim();
|
||||
}
|
||||
|
||||
+17
-14
@@ -1,9 +1,9 @@
|
||||
import _url from 'url';
|
||||
|
||||
import { ConfigError, getExpoSDKVersion } from '@expo/config';
|
||||
import { ExponentTools } from '@expo/xdl';
|
||||
import { ConfigError, getExpoSDKVersion, ProjectConfig } from '@expo/config';
|
||||
import _ from 'lodash';
|
||||
import uuid from 'uuid';
|
||||
import { ExponentTools } from '../../xdl';
|
||||
|
||||
import { ErrorWithCommandHelp } from 'turtle/bin/commands/ErrorWithCommandHelp';
|
||||
import { ErrorWithProgramHelp } from 'turtle/bin/commands/ErrorWithProgramHelp';
|
||||
@@ -57,7 +57,7 @@ export function createBuilderAction({
|
||||
);
|
||||
}
|
||||
|
||||
if (cmd.publicUrl) {
|
||||
if (cmd.publicUrl && !cmd.allowNonHttpsPublicUrl) {
|
||||
const parsedPublicUrl = _url.parse(cmd.publicUrl);
|
||||
if (parsedPublicUrl.protocol !== 'https:') {
|
||||
throw new ErrorWithCommandHelp('--public-url is invalid - only HTTPS urls are supported');
|
||||
@@ -74,12 +74,12 @@ export function createBuilderAction({
|
||||
publicUrl: cmd.publicUrl,
|
||||
};
|
||||
|
||||
const appJSON = await ProjectUtils.loadAppJSON(projectDirArg, cmd.config);
|
||||
const sdkVersion = getExpoSDKVersionSafely(projectAbsoluteDir, appJSON);
|
||||
const projectConfig = await ProjectUtils.getConfig(projectDirArg, cmd.config);
|
||||
const sdkVersion = getExpoSDKVersionSafely(projectAbsoluteDir, projectConfig);
|
||||
await setup(platform, sdkVersion);
|
||||
const credentials = await prepareCredentials(cmd);
|
||||
const rawJob = {
|
||||
...await buildJobObject(platform, appJSON, args, credentials, sdkVersion),
|
||||
...await buildJobObject(cmd, platform, projectConfig, args, credentials, sdkVersion),
|
||||
...cmd.buildDir && { fakeUploadDir: ProjectUtils.resolveAbsoluteDir(cmd.buildDir) },
|
||||
...cmd.output && { fakeUploadBuildPath: ProjectUtils.resolveAbsoluteDir(cmd.output) },
|
||||
};
|
||||
@@ -97,9 +97,9 @@ export function createBuilderAction({
|
||||
};
|
||||
}
|
||||
|
||||
function getExpoSDKVersionSafely(projectDir: string, appJSON?: any) {
|
||||
function getExpoSDKVersionSafely(projectDir: string, projectConfig: ProjectConfig) {
|
||||
try {
|
||||
return getExpoSDKVersion(projectDir, appJSON && appJSON.expo);
|
||||
return getExpoSDKVersion(projectDir, projectConfig.exp);
|
||||
} catch (err) {
|
||||
if (err instanceof ConfigError) {
|
||||
throw new Error(
|
||||
@@ -113,22 +113,25 @@ function getExpoSDKVersionSafely(projectDir: string, appJSON?: any) {
|
||||
}
|
||||
|
||||
const buildJobObject = async (
|
||||
cmd: any,
|
||||
platform: 'android' | 'ios',
|
||||
appJSON: any,
|
||||
projectConfig: ProjectConfig,
|
||||
{ releaseChannel, buildType, buildMode, username, publicUrl, projectDir }: any,
|
||||
credentials: any,
|
||||
sdkVersion: string,
|
||||
) => {
|
||||
const experienceName = `@${_.get(appJSON, 'expo.owner', username)}/${_.get(appJSON, 'expo.slug')}`;
|
||||
const owner = projectConfig.exp.owner || username;
|
||||
const experienceName = `@${owner}/${projectConfig.exp.slug}`;
|
||||
const job = {
|
||||
config: {
|
||||
..._.get(appJSON, `expo.${platform}.config`, {}),
|
||||
...(projectConfig.exp?.[platform]?.config || {}),
|
||||
buildType,
|
||||
...(platform === 'android' ? { buildMode } : {}),
|
||||
releaseChannel,
|
||||
...(platform === 'ios' ? { bundleIdentifier: _.get(appJSON, 'expo.ios.bundleIdentifier') } : {}),
|
||||
...(platform === 'android' ? { androidPackage: _.get(appJSON, 'expo.android.package') } : {}),
|
||||
publicUrl,
|
||||
...(platform === 'android' && cmd.gradleArgs ? { gradleArgs: cmd.gradleArgs.split(' ') } : {}),
|
||||
...(platform === 'android' ? { buildMode } : {}),
|
||||
...(platform === 'android' ? { androidPackage: projectConfig.exp?.android?.package } : {}),
|
||||
...(platform === 'ios' ? { bundleIdentifier: projectConfig.exp?.ios?.bundleIdentifier } : {}),
|
||||
},
|
||||
id: uuid.v4(),
|
||||
platform,
|
||||
|
||||
+11
-13
@@ -1,20 +1,18 @@
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
|
||||
export async function loadAppJSON(projectDirArg: string, config: string | null) {
|
||||
let appJSONPath;
|
||||
import { getConfig as getProjectConfig, setCustomConfigPath } from '@expo/config';
|
||||
import fs from 'fs-extra';
|
||||
|
||||
export async function getConfig(projectDir: string, config: string | null) {
|
||||
projectDir = resolveAbsoluteDir(projectDir);
|
||||
if (config) {
|
||||
appJSONPath = resolveAbsoluteDir(config);
|
||||
} else {
|
||||
const projectDir = resolveAbsoluteDir(projectDirArg);
|
||||
appJSONPath = path.join(projectDir, 'app.json');
|
||||
}
|
||||
const appJSONExists = await fs.pathExists(appJSONPath);
|
||||
if (!appJSONExists) {
|
||||
throw new Error(`Couldn't find app.json.`);
|
||||
} else {
|
||||
return require(appJSONPath);
|
||||
const pathToConfig = path.resolve(process.cwd(), config);
|
||||
if (!await fs.pathExists(pathToConfig)) {
|
||||
throw new Error(`File at provided config path does not exist: ${pathToConfig}`);
|
||||
}
|
||||
setCustomConfigPath(projectDir, pathToConfig);
|
||||
}
|
||||
return await getProjectConfig(projectDir, {});
|
||||
}
|
||||
|
||||
export const resolveAbsoluteDir = (dir: string) => {
|
||||
|
||||
+18
-12
@@ -1,9 +1,9 @@
|
||||
import path from 'path';
|
||||
|
||||
import { AndroidShellApp, ImageUtils } from '@expo/xdl';
|
||||
import { AndroidShellApp, ImageUtils } from '../xdl';
|
||||
|
||||
import fs from 'fs-extra';
|
||||
import _ from 'lodash';
|
||||
import semver from 'semver';
|
||||
import { ANDROID_BUILD_TYPES } from 'turtle/constants';
|
||||
import uuidv4 from 'uuid/v4';
|
||||
|
||||
@@ -11,7 +11,7 @@ import getOrCreateCredentials from 'turtle/builders/utils/android/credentials';
|
||||
import { formatShellAppDirectory } from 'turtle/builders/utils/android/workingdir';
|
||||
import * as commonUtils from 'turtle/builders/utils/common';
|
||||
import * as imageHelpers from 'turtle/builders/utils/image';
|
||||
import { resolveExplicitOptIn } from 'turtle/builders/utils/unimodules';
|
||||
import { resolveExplicitOptIn, resolveNativeModules } from 'turtle/builders/utils/unimodules';
|
||||
import { uploadBuildToS3 } from 'turtle/builders/utils/uploader';
|
||||
import { ensureCanBuildSdkVersion } from 'turtle/builders/utils/version';
|
||||
import config from 'turtle/config';
|
||||
@@ -35,8 +35,8 @@ export default async function buildAndroid(jobData: IJob): Promise<IJobResult> {
|
||||
...config.builder.fakeUpload && {
|
||||
fakeUploadBuildPath:
|
||||
jobData.fakeUploadBuildPath
|
||||
? jobData.fakeUploadBuildPath
|
||||
: path.join(jobData.fakeUploadDir || config.directories.fakeUploadDir, fakeUploadFilename),
|
||||
? jobData.fakeUploadBuildPath
|
||||
: path.join(jobData.fakeUploadDir || config.directories.fakeUploadDir, fakeUploadFilename),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -73,15 +73,20 @@ async function runShellAppBuilder(
|
||||
const sdkVersion = _.get(manifest, 'sdkVersion', sdkVersionFromJob);
|
||||
const workingDir = formatShellAppDirectory({ sdkVersion });
|
||||
|
||||
// (2019-07-31) We are explicitly choosing to disable this option until we have more
|
||||
// infrastructure/tooling built around optional modules and OTA updates, as right now it's
|
||||
// very easy for developers to break apps in production with optional modules.
|
||||
// Default behaviour:
|
||||
// - load all modules except [expo-branch]
|
||||
// - load expo-branch only if present in package.json dependencies
|
||||
//
|
||||
// to enable full resolver switch resolveExplicitOptIn to resolveNativeModules
|
||||
// Behaviour with "enableDangerousExperimentalLeanBuilds" flag enabled
|
||||
// - add unimodules from `manifest.dependencies` (project package.json dependecies)
|
||||
// - add unimodules from react-native-unimodules package.json
|
||||
// - add unimodules from expo package package.json
|
||||
// - add recusively all dependecies of already added unimodules
|
||||
logger.info({ buildPhase: 'resolve native modules' }, 'Resolving universal modules dependencies');
|
||||
const enabledModules = semver.satisfies(sdkVersion, '>= 33.0.0')
|
||||
? await resolveExplicitOptIn(workingDir, _.get(manifest, 'dependencies'))
|
||||
: null;
|
||||
const packageJsonDependecies = _.get(manifest, 'dependencies');
|
||||
const enabledModules = _.get(manifest, 'android.enableDangerousExperimentalLeanBuilds')
|
||||
? await resolveNativeModules(workingDir, sdkVersion, packageJsonDependecies)
|
||||
: await resolveExplicitOptIn(workingDir, packageJsonDependecies);
|
||||
|
||||
try {
|
||||
await AndroidShellApp.createAndroidShellAppAsync({
|
||||
@@ -99,6 +104,7 @@ async function runShellAppBuilder(
|
||||
modules: enabledModules,
|
||||
buildType: jobData.config.buildType,
|
||||
buildMode: jobData.config.buildMode,
|
||||
gradleArgs: jobData.config.gradleArgs,
|
||||
});
|
||||
} catch (err) {
|
||||
commonUtils.logErrorOnce(err);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { IosIPABuilder as createIPABuilder } from '@expo/xdl';
|
||||
import _ from 'lodash';
|
||||
import { IosIPABuilder as createIPABuilder } from '../../xdl';
|
||||
|
||||
import { IContext } from 'turtle/builders/ios/context';
|
||||
import { logErrorOnce } from 'turtle/builders/utils/common';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as path from 'path';
|
||||
|
||||
import { ExponentTools, IosShellApp } from '@expo/xdl';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { ExponentTools, IosShellApp } from '../../xdl';
|
||||
|
||||
import { formatShellAppDirectory } from 'turtle/builders/utils/ios/workingdir';
|
||||
import config from 'turtle/config';
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import fs from 'fs-extra';
|
||||
import pick from 'lodash/pick';
|
||||
|
||||
import spawnAsync from '@expo/spawn-async';
|
||||
import buildArchive from 'turtle/builders/ios/archive';
|
||||
import { createBuilderContext, IContext } from 'turtle/builders/ios/context';
|
||||
import buildSimulator from 'turtle/builders/ios/simulator';
|
||||
@@ -51,6 +52,13 @@ async function initBuilder(ctx: IContext) {
|
||||
await fs.ensureDir(dir);
|
||||
await fs.chmod(dir, 0o755);
|
||||
}
|
||||
|
||||
if (config.builder.mode === 'online') {
|
||||
await spawnAsync('sudo', ['xcrun', 'simctl', 'list']);
|
||||
} else {
|
||||
// sudo not needed for CLI builds
|
||||
await spawnAsync('xcrun', ['simctl', 'list']);
|
||||
}
|
||||
}
|
||||
|
||||
async function cleanup(ctx: IContext) {
|
||||
|
||||
@@ -5,11 +5,10 @@ import fs from 'fs-extra';
|
||||
import get from 'lodash/get';
|
||||
import uuidv4 from 'uuid/v4';
|
||||
|
||||
import * as sqs from 'turtle/aws/sqs';
|
||||
import { UPDATE_CREDENTIALS } from 'turtle/constants/build';
|
||||
import { IAndroidCredentials, IJob } from 'turtle/job';
|
||||
import logger from 'turtle/logger';
|
||||
import { isOffline } from 'turtle/turtleContext';
|
||||
import { sendCredentialsUpdate } from 'turtle/utils/www';
|
||||
|
||||
async function getOrCreateCredentials(jobData: IJob): Promise<IAndroidCredentials> {
|
||||
if (!jobData.credentials) {
|
||||
@@ -50,9 +49,7 @@ async function getOrCreateCredentials(jobData: IJob): Promise<IAndroidCredential
|
||||
l.info(`Key password: ${credentials.keyPassword}`);
|
||||
l.info(`Please keep these credentials safe`);
|
||||
} else {
|
||||
await sqs.sendMessage(jobData.id, UPDATE_CREDENTIALS, {
|
||||
...credentials,
|
||||
});
|
||||
await sendCredentialsUpdate(jobData.id, 'android', credentials);
|
||||
l.info('Keystore sent to storage successfully');
|
||||
}
|
||||
await fs.unlink(keystoreFilename);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import path from 'path';
|
||||
|
||||
import { ExponentTools } from '@expo/xdl';
|
||||
import { ExponentTools } from '../../../xdl';
|
||||
|
||||
import { IShellAppDirectoryConfig } from 'turtle/builders/utils/workingdir';
|
||||
import config from 'turtle/config';
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
import path from 'path';
|
||||
|
||||
import spawnAsync from '@expo/spawn-async';
|
||||
import { IosCodeSigning } from '@expo/xdl';
|
||||
import fs from 'fs-extra';
|
||||
import { IosCodeSigning } from '../../../xdl';
|
||||
|
||||
import * as sqs from 'turtle/aws/sqs';
|
||||
import BuildError, { BuildErrorReason } from 'turtle/builders/BuildError';
|
||||
import { UPDATE_CREDENTIALS } from 'turtle/constants/build';
|
||||
import { IJob } from 'turtle/job';
|
||||
import logger from 'turtle/logger';
|
||||
import { isOffline } from 'turtle/turtleContext';
|
||||
import { sendCredentialsUpdate } from 'turtle/utils/www';
|
||||
|
||||
// tslint:disable-next-line:no-var-requires
|
||||
const travelingFastlane = process.platform === 'darwin' ? require('@expo/traveling-fastlane-darwin')() : null;
|
||||
@@ -64,7 +63,7 @@ async function prepareAdHocBuildCredentials(job: IJob) {
|
||||
await fs.writeFile(provisioningProfilePath, Buffer.from(credentials.provisioningProfile, 'base64'));
|
||||
logger.info(`Saved provisioning profile to ${provisioningProfilePath}`);
|
||||
} else {
|
||||
await sqs.sendMessage(job.id, UPDATE_CREDENTIALS, {
|
||||
await sendCredentialsUpdate(job.id, 'ios', {
|
||||
provisioningProfileId: credentials.provisioningProfileId,
|
||||
provisioningProfile: credentials.provisioningProfile,
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { IosKeychain } from '@expo/xdl';
|
||||
import { IosKeychain } from '../../../xdl';
|
||||
|
||||
import { IContext } from 'turtle/builders/ios/context';
|
||||
import * as fileUtils from 'turtle/builders/utils/file';
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import path from 'path';
|
||||
import util from 'util';
|
||||
|
||||
import { ImageUtils, IosShellApp } from '@expo/xdl';
|
||||
import copy from 'copy';
|
||||
import _ from 'lodash';
|
||||
import { ImageUtils, IosShellApp } from '../../../xdl';
|
||||
|
||||
import { IContext } from 'turtle/builders/ios/context';
|
||||
import * as commonUtils from 'turtle/builders/utils/common';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import path from 'path';
|
||||
|
||||
import { ExponentTools } from '@expo/xdl';
|
||||
import { ExponentTools } from '../../../xdl';
|
||||
|
||||
import { IShellAppDirectoryConfig } from 'turtle/builders/utils/workingdir';
|
||||
import config from 'turtle/config';
|
||||
|
||||
@@ -4,6 +4,7 @@ import keys from 'lodash/keys';
|
||||
import merge from 'lodash/merge';
|
||||
import values from 'lodash/values';
|
||||
import path from 'path';
|
||||
import semver from 'semver';
|
||||
import logger from 'turtle/logger';
|
||||
|
||||
export interface IUnimoduleEntry {
|
||||
@@ -19,21 +20,37 @@ interface IPackageInfo {
|
||||
dependencies: string[];
|
||||
}
|
||||
|
||||
export async function resolveExplicitOptIn(workingdir: string, packages?: string[]): Promise<IUnimoduleEntry[]> {
|
||||
export async function resolveExplicitOptIn(
|
||||
workingdir: string,
|
||||
packages?: string[],
|
||||
): Promise<IUnimoduleEntry[]> {
|
||||
const resolver = new Resolver(workingdir);
|
||||
await resolver.init();
|
||||
|
||||
const optionalModules = ['expo-branch'];
|
||||
resolver.addAll();
|
||||
const modules = packages
|
||||
? resolver.getModules().filter((mod) => !optionalModules.includes(mod.name) || packages.includes(mod.name))
|
||||
: resolver.getModules().filter((mod) => !optionalModules.includes(mod.name));
|
||||
? resolver
|
||||
.getModules()
|
||||
.filter(
|
||||
(mod) =>
|
||||
!optionalModules.includes(mod.name) || packages.includes(mod.name),
|
||||
)
|
||||
: resolver
|
||||
.getModules()
|
||||
.filter((mod) => !optionalModules.includes(mod.name));
|
||||
|
||||
modules.forEach(({ name, version }) => { logger.info(`Adding ${name}:${version}`); });
|
||||
modules.forEach(({ name, version }) => {
|
||||
logger.info(`Adding ${name}:${version}`);
|
||||
});
|
||||
return modules;
|
||||
}
|
||||
|
||||
export async function resolveNativeModules(workingdir: string, packages?: string[]): Promise<IUnimoduleEntry[]> {
|
||||
export async function resolveNativeModules(
|
||||
workingdir: string,
|
||||
sdkVersion: string,
|
||||
packages?: string[],
|
||||
): Promise<IUnimoduleEntry[]> {
|
||||
const resolver = new Resolver(workingdir);
|
||||
await resolver.init();
|
||||
|
||||
@@ -41,6 +58,13 @@ export async function resolveNativeModules(workingdir: string, packages?: string
|
||||
resolver.addAll();
|
||||
} else {
|
||||
const corePackages = await readCorePackages(workingdir);
|
||||
|
||||
// expo-notifications is required for shell apps in SDK >= 39
|
||||
// https://github.com/expo/expo/issues/10569
|
||||
if (semver.gte(sdkVersion, '39.0.0')) {
|
||||
resolver.addModule('expo-notifications');
|
||||
}
|
||||
|
||||
for (const pkg of corePackages) {
|
||||
if (resolver.isUnimodule(pkg)) {
|
||||
resolver.addModule(pkg);
|
||||
@@ -53,17 +77,36 @@ export async function resolveNativeModules(workingdir: string, packages?: string
|
||||
}
|
||||
}
|
||||
const modules = resolver.getModules();
|
||||
modules.forEach(({ name, version }) => { logger.info(`Adding ${name}:${version}`); });
|
||||
modules.forEach(({ name, version }) => {
|
||||
logger.info(`Adding ${name}:${version}`);
|
||||
});
|
||||
return modules;
|
||||
}
|
||||
|
||||
async function readCorePackages(workingdir: string) {
|
||||
const pkgJsonPath = path.join(workingdir, 'node_modules', 'react-native-unimodules', 'package.json');
|
||||
const expoPkgJsonPath = path.join(workingdir, 'packages', 'expo', 'package.json');
|
||||
if (await fs.pathExists(pkgJsonPath) && await fs.pathExists(expoPkgJsonPath)) {
|
||||
const pkgJsonPath = path.join(
|
||||
workingdir,
|
||||
'node_modules',
|
||||
'react-native-unimodules',
|
||||
'package.json',
|
||||
);
|
||||
const expoPkgJsonPath = path.join(
|
||||
workingdir,
|
||||
'packages',
|
||||
'expo',
|
||||
'package.json',
|
||||
);
|
||||
if (
|
||||
(await fs.pathExists(pkgJsonPath)) &&
|
||||
(await fs.pathExists(expoPkgJsonPath))
|
||||
) {
|
||||
const packageJson = JSON.parse(await fs.readFile(pkgJsonPath, 'utf-8'));
|
||||
const expoPackageJson = JSON.parse(await fs.readFile(expoPkgJsonPath, 'utf-8'));
|
||||
return keys(packageJson.dependencies).concat(keys(expoPackageJson.dependencies));
|
||||
const expoPackageJson = JSON.parse(
|
||||
await fs.readFile(expoPkgJsonPath, 'utf-8'),
|
||||
);
|
||||
return keys(packageJson.dependencies).concat(
|
||||
keys(expoPackageJson.dependencies),
|
||||
);
|
||||
} else {
|
||||
logger.warn('No core packages detected');
|
||||
return [];
|
||||
@@ -76,7 +119,7 @@ interface IDependencyInfo {
|
||||
|
||||
class Resolver {
|
||||
private workingdir: string;
|
||||
private modulesMap: {[key: string]: IUnimoduleEntry};
|
||||
private modulesMap: { [key: string]: IUnimoduleEntry };
|
||||
private dependencyMap: IDependencyInfo;
|
||||
|
||||
constructor(workingdir: string) {
|
||||
@@ -108,7 +151,7 @@ class Resolver {
|
||||
}
|
||||
|
||||
public getModules(): IUnimoduleEntry[] {
|
||||
return values(this.modulesMap);
|
||||
return values(this.modulesMap);
|
||||
}
|
||||
|
||||
private async resolveModule(moduleName: string) {
|
||||
@@ -127,7 +170,9 @@ class Resolver {
|
||||
}
|
||||
}
|
||||
|
||||
async function generateDependenciesInfo(workingdir: string): Promise<IDependencyInfo> {
|
||||
async function generateDependenciesInfo(
|
||||
workingdir: string,
|
||||
): Promise<IDependencyInfo> {
|
||||
const dir = await fs.readdir(path.join(workingdir, 'packages'));
|
||||
const map: IDependencyInfo = {};
|
||||
|
||||
@@ -140,7 +185,9 @@ async function generateDependenciesInfo(workingdir: string): Promise<IDependency
|
||||
}
|
||||
|
||||
// handle @unimodules directory
|
||||
const unimodulesDir = await fs.readdir(path.join(workingdir, 'packages', '@unimodules'));
|
||||
const unimodulesDir = await fs.readdir(
|
||||
path.join(workingdir, 'packages', '@unimodules'),
|
||||
);
|
||||
for (const pkgName of unimodulesDir) {
|
||||
const pkg = await getPackage(workingdir, path.join('@unimodules', pkgName));
|
||||
if (pkg) {
|
||||
@@ -155,19 +202,27 @@ async function generateDependenciesInfo(workingdir: string): Promise<IDependency
|
||||
return map;
|
||||
}
|
||||
|
||||
async function getPackage(workingdir: string, pkgName: string): Promise<IPackageInfo | null> {
|
||||
async function getPackage(
|
||||
workingdir: string,
|
||||
pkgName: string,
|
||||
): Promise<IPackageInfo | null> {
|
||||
const pkgPath = path.join(workingdir, 'packages', pkgName);
|
||||
const isAndroid = await fs.pathExists(path.join(pkgPath, 'android'));
|
||||
const isIos = await fs.pathExists(path.join(pkgPath, 'ios'));
|
||||
const isUnimodule = await fs.pathExists(path.join(pkgPath, 'unimodule.json'));
|
||||
const hasPackageJson = await fs.pathExists(path.join(pkgPath, 'package.json'));
|
||||
const hasPackageJson = await fs.pathExists(
|
||||
path.join(pkgPath, 'package.json'),
|
||||
);
|
||||
if (!isUnimodule || !hasPackageJson || (!isAndroid && !isIos)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const packageJson = JSON.parse(await fs.readFile(path.join(pkgPath, 'package.json'), 'utf-8'));
|
||||
const packageJson = JSON.parse(
|
||||
await fs.readFile(path.join(pkgPath, 'package.json'), 'utf-8'),
|
||||
);
|
||||
|
||||
const pkgDeps = merge({}, // reading everything to make sure
|
||||
const pkgDeps = merge(
|
||||
{}, // reading everything to make sure
|
||||
packageJson.dependencies,
|
||||
packageJson.peerDependencies,
|
||||
packageJson.unimodulePeerDependencies,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ExponentTools } from '@expo/xdl';
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
import { ExponentTools } from '../../xdl';
|
||||
|
||||
import config from 'turtle/config';
|
||||
import { IJob } from 'turtle/job';
|
||||
|
||||
+18
-5
@@ -1,3 +1,4 @@
|
||||
import fs from 'fs';
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
|
||||
@@ -28,7 +29,6 @@ export default {
|
||||
ios: env('AWS_SQS_IOS_PRIORITY_QUEUE_URL'),
|
||||
android: env('AWS_SQS_ANDROID_PRIORITY_QUEUE_URL'),
|
||||
},
|
||||
out: env('AWS_SQS_OUT_QUEUE_URL'),
|
||||
},
|
||||
},
|
||||
cloudwatch: {
|
||||
@@ -44,8 +44,17 @@ export default {
|
||||
intervalMs: env('DATADOG_INTERVAL_MS', 5 * 60 * 1000),
|
||||
},
|
||||
redis: {
|
||||
url: env('REDIS_URL'),
|
||||
configUrl: env('REDIS_CONFIG_URL'),
|
||||
main: {
|
||||
url: env('REDIS_URL'),
|
||||
caCertificate: envTransform(
|
||||
'REDIS_CA_CERTIFICATE',
|
||||
null,
|
||||
(caCertificate: string) => caCertificate && fs.readFileSync(caCertificate),
|
||||
),
|
||||
},
|
||||
turtle: {
|
||||
url: env('REDIS_CONFIG_URL'),
|
||||
},
|
||||
},
|
||||
logger: {
|
||||
level: env('LOGGER_LEVEL', 'info'),
|
||||
@@ -81,7 +90,7 @@ export default {
|
||||
androidDependenciesDir: env(
|
||||
'TURTLE_ANDROID_DEPENDENCIES_DIR',
|
||||
path.join(os.homedir(), '.turtle/androidDependencies'),
|
||||
),
|
||||
),
|
||||
tempS3LogsDir: env('TURTLE_TEMP_S3_LOGS_DIR', '/tmp/logs'),
|
||||
fakeUploadDir: envOptional('TURTLE_FAKE_UPLOAD_DIR'),
|
||||
temporaryFilesRoot: env(
|
||||
@@ -90,6 +99,10 @@ export default {
|
||||
),
|
||||
},
|
||||
www: {
|
||||
sdkVersionsSecretToken: env('TURTLE_SDK_VERSIONS_SECRET_TOKEN'),
|
||||
secretToken: env('TURTLE_SDK_VERSIONS_SECRET_TOKEN'),
|
||||
},
|
||||
leader: {
|
||||
redisKey: env('LEADER_REDIS_KEY', 'turtle-v1:leader'),
|
||||
redisKeyTTLSec: envNum('LEADER_REDIS_KEY_TTL', 30),
|
||||
},
|
||||
};
|
||||
@@ -15,6 +15,7 @@ export interface IJob {
|
||||
// android
|
||||
androidPackage?: string;
|
||||
buildMode?: ANDROID_BUILD_MODES;
|
||||
gradleArgs?: string[];
|
||||
};
|
||||
credentials: {
|
||||
// android
|
||||
|
||||
+26
-10
@@ -12,6 +12,7 @@ import * as buildStatusMetric from 'turtle/metrics/buildStatus';
|
||||
import { checkShouldExit, setCurrentJobId, turtleVersion } from 'turtle/turtleContext';
|
||||
import { getPriorities, labelConfiguration, TurtleMode } from 'turtle/utils/priorities';
|
||||
import * as redis from 'turtle/utils/redis';
|
||||
import { sendBuildStatusUpdate } from 'turtle/utils/www';
|
||||
import { sanitizeJob } from 'turtle/validator';
|
||||
|
||||
function _maybeExit() {
|
||||
@@ -73,7 +74,8 @@ async function processJob(jobData: any, turtleMode: TurtleMode) {
|
||||
logger.error({ err, ...rawJob && { buildJobId: rawJob.id } }, 'The build job is invalid');
|
||||
// send message only if we've managed to parse the job json
|
||||
if (rawJob) {
|
||||
await sqs.sendMessage(rawJob.id, BUILD.JOB_STATES.ERRORED, {
|
||||
await sendBuildStatusUpdate(rawJob.id, {
|
||||
status: BUILD.JOB_STATES.ERRORED,
|
||||
turtleVersion,
|
||||
buildDuration: 0,
|
||||
reason: 'The build job is invalid',
|
||||
@@ -128,7 +130,7 @@ async function processJob(jobData: any, turtleMode: TurtleMode) {
|
||||
function failAfterMaxJobTime(priority: string, receiptHandle: string, job: any) {
|
||||
return setTimeout(async () => {
|
||||
try {
|
||||
sqs.sendMessage(job.id, BUILD.JOB_STATES.ERRORED, { turtleVersion });
|
||||
sendBuildStatusUpdate(job.id, { status: BUILD.JOB_STATES.ERRORED, turtleVersion });
|
||||
await deleteMessage(priority, receiptHandle);
|
||||
} finally {
|
||||
logger.error('Build timed out. Going to terminate turtle agent.');
|
||||
@@ -143,16 +145,28 @@ async function build(job: any, turtleMode: TurtleMode) {
|
||||
const s3Url = await logger.initForJob(job);
|
||||
const calculateBuildDuration = () => Math.ceil((Date.now() - startTimestamp) / 1000);
|
||||
|
||||
try {
|
||||
await sqs.sendMessage(job.id, BUILD.JOB_STATES.IN_PROGRESS, {
|
||||
logUrl: s3Url,
|
||||
logFormat: 'json',
|
||||
const commonPayload = {
|
||||
logs: {
|
||||
url: s3Url,
|
||||
format: 'json',
|
||||
},
|
||||
extraData: {
|
||||
turtleMode,
|
||||
},
|
||||
};
|
||||
try {
|
||||
await sendBuildStatusUpdate(job.id, {
|
||||
...commonPayload,
|
||||
status: BUILD.JOB_STATES.IN_PROGRESS,
|
||||
});
|
||||
const result = await (builders as any)[job.platform](job);
|
||||
const { artifactUrl } = await (builders as any)[job.platform](job);
|
||||
const buildDuration = calculateBuildDuration();
|
||||
sqs.sendMessage(job.id, BUILD.JOB_STATES.FINISHED, {
|
||||
...result,
|
||||
sendBuildStatusUpdate(job.id, {
|
||||
...commonPayload,
|
||||
artifacts: {
|
||||
url: artifactUrl,
|
||||
},
|
||||
status: BUILD.JOB_STATES.FINISHED,
|
||||
turtleVersion,
|
||||
buildDuration,
|
||||
});
|
||||
@@ -164,7 +178,9 @@ async function build(job: any, turtleMode: TurtleMode) {
|
||||
if (err instanceof BuildError) {
|
||||
reason = err.reason;
|
||||
}
|
||||
sqs.sendMessage(job.id, BUILD.JOB_STATES.ERRORED, {
|
||||
sendBuildStatusUpdate(job.id, {
|
||||
...commonPayload,
|
||||
status: BUILD.JOB_STATES.ERRORED,
|
||||
turtleVersion,
|
||||
buildDuration,
|
||||
...reason && { reason },
|
||||
|
||||
@@ -15,6 +15,7 @@ export default baseJobSchema.concat(
|
||||
config: Joi.object().keys({
|
||||
buildMode: Joi.string().valid(Object.values(ANDROID_BUILD_MODES)).default(ANDROID_BUILD_MODES.RELEASE),
|
||||
buildType: Joi.string().valid(Object.values(ANDROID_BUILD_TYPES)).default(ANDROID_BUILD_TYPES.APK),
|
||||
gradleArgs: Joi.array().items(Joi.string()),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
+44
-7
@@ -1,14 +1,25 @@
|
||||
import { LoggerDetach } from '@expo/xdl';
|
||||
import { LoggerDetach } from './xdl';
|
||||
|
||||
import { findSupportedSdkVersions } from 'turtle/builders/utils/version';
|
||||
import config from 'turtle/config';
|
||||
import { doJob } from 'turtle/jobManager';
|
||||
import logger from 'turtle/logger';
|
||||
import setup from 'turtle/setup';
|
||||
import { checkShouldExit, setShouldExit, turtleVersion } from 'turtle/turtleContext';
|
||||
import { setSupportedSdkVersions, setTurtleVersion } from 'turtle/utils/versions';
|
||||
import {
|
||||
checkShouldExit,
|
||||
setShouldExit,
|
||||
turtleVersion,
|
||||
} from 'turtle/turtleContext';
|
||||
import { synchronizeFailedUpdates } from 'turtle/updatesManager';
|
||||
import Leader, { LeaderEvent } from 'turtle/utils/leader';
|
||||
import {
|
||||
setSupportedSdkVersions,
|
||||
setTurtleVersion,
|
||||
} from 'turtle/utils/versions';
|
||||
|
||||
process.on('unhandledRejection', (err) => logger.error({ err }, 'Unhandled promise rejection'));
|
||||
process.on('unhandledRejection', (err) =>
|
||||
logger.error({ err }, 'Unhandled promise rejection'),
|
||||
);
|
||||
|
||||
function handleExit() {
|
||||
if (checkShouldExit()) {
|
||||
@@ -25,18 +36,23 @@ process.on('SIGINT', handleExit);
|
||||
|
||||
async function main() {
|
||||
logger.info(
|
||||
'Starting Turtle... '
|
||||
+ `NODE_ENV=${config.env}, PLATFORM=${config.platform}, DEPLOYMENT_ENVIRONMENT=${config.deploymentEnv}`,
|
||||
'Starting Turtle... ' +
|
||||
`NODE_ENV=${config.env}, PLATFORM=${config.platform}, DEPLOYMENT_ENVIRONMENT=${config.deploymentEnv}`,
|
||||
);
|
||||
// @ts-ignore: Type mismatch
|
||||
LoggerDetach.configure(logger);
|
||||
if (setup[config.platform]) {
|
||||
await setup[config.platform]();
|
||||
}
|
||||
|
||||
await electLeaderAndSynchronize();
|
||||
|
||||
try {
|
||||
await setTurtleVersion(turtleVersion);
|
||||
logger.info(`Registered Turtle version (${turtleVersion}) in www`);
|
||||
const sdkVersions = (await findSupportedSdkVersions(config.platform)).map((version) => `${version}.0.0`);
|
||||
const sdkVersions = (await findSupportedSdkVersions(config.platform)).map(
|
||||
(version) => `${version}.0.0`,
|
||||
);
|
||||
await setSupportedSdkVersions(config.platform, sdkVersions);
|
||||
logger.info(`Registered supported SDK versions (${sdkVersions}) in www`);
|
||||
} catch (err) {
|
||||
@@ -52,6 +68,27 @@ async function main() {
|
||||
}
|
||||
}
|
||||
|
||||
async function electLeaderAndSynchronize() {
|
||||
if (config.env === 'test') {
|
||||
return;
|
||||
}
|
||||
const l = new Leader({
|
||||
key: config.leader.redisKey,
|
||||
id: config.hostname,
|
||||
ttl: config.leader.redisKeyTTLSec,
|
||||
});
|
||||
let intervalId: NodeJS.Timeout;
|
||||
l.on(LeaderEvent.elected, async () => {
|
||||
intervalId = await synchronizeFailedUpdates();
|
||||
});
|
||||
l.on(LeaderEvent.revoked, () => {
|
||||
if (intervalId) {
|
||||
clearInterval(intervalId);
|
||||
}
|
||||
});
|
||||
await l.elect();
|
||||
}
|
||||
|
||||
main()
|
||||
.then(() => logger.error('This should never happen...'))
|
||||
.catch((err) => logger.error({ err }, 'Something went terribly wrong'))
|
||||
|
||||
Vendored
-1
@@ -4,7 +4,6 @@ declare module 'bunyan-sentry-stream';
|
||||
declare module 'bunyan-debug-stream';
|
||||
declare module 'copy';
|
||||
declare module 'dogapi';
|
||||
declare module 'ioredis';
|
||||
declare module 'is-semver';
|
||||
declare module 'request-progress';
|
||||
declare module '@expo/xdl';
|
||||
Vendored
+472
@@ -0,0 +1,472 @@
|
||||
interface pbxFile {
|
||||
basename: string;
|
||||
lastKnownFileType?: string;
|
||||
group?: string;
|
||||
path?: string;
|
||||
fileEncoding?: number;
|
||||
defaultEncoding?: number;
|
||||
sourceTree: string;
|
||||
includeInIndex?: number;
|
||||
explicitFileType?: unknown;
|
||||
settings?: object;
|
||||
uuid?: string;
|
||||
fileRef: string;
|
||||
target?: string;
|
||||
}
|
||||
|
||||
declare module 'xcode' {
|
||||
/**
|
||||
* UUID that is a key to each fragment of PBXProject.
|
||||
*/
|
||||
type UUID = string;
|
||||
|
||||
/**
|
||||
* if has following format `${UUID}_comment`
|
||||
*/
|
||||
type UUIDComment = string;
|
||||
|
||||
type XCObjectType =
|
||||
| 'PBXBuildFile'
|
||||
| 'PBXFileReference'
|
||||
| 'PBXFrameworksBuildPhase'
|
||||
| 'PBXGroup'
|
||||
| 'PBXNativeTarget'
|
||||
| 'PBXProject'
|
||||
| 'PBXResourcesBuildPhase'
|
||||
| 'PBXShellScriptBuildPhase'
|
||||
| 'PBXSourcesBuildPhase'
|
||||
| 'PBXVariantGroup'
|
||||
| 'XCBuildConfiguration'
|
||||
| 'XCConfigurationList';
|
||||
|
||||
type PBXFile = pbxFile;
|
||||
|
||||
interface PBXProject {
|
||||
isa: 'PBXProject';
|
||||
attributes: {
|
||||
LastUpgradeCheck: number;
|
||||
TargetAttributes: Record<
|
||||
UUID,
|
||||
{
|
||||
CreatedOnToolsVersion?: string;
|
||||
TestTargetID?: UUID;
|
||||
LastSwiftMigration?: number;
|
||||
ProvisioningStyle?: string;
|
||||
} & Record<string, undefined | number | string>
|
||||
>;
|
||||
};
|
||||
buildConfigurationList: UUID;
|
||||
buildConfigurationList_comment: string;
|
||||
compatibilityVersion: string;
|
||||
developmentRegion: string;
|
||||
hasScannedForEncodings: number;
|
||||
knownRegions: string[];
|
||||
mainGroup: UUID;
|
||||
productRefGroup: UUID;
|
||||
productRefGroup_comment: string;
|
||||
projectDirPath: string;
|
||||
projectRoot: string;
|
||||
targets: Array<{
|
||||
value: UUID;
|
||||
comment: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
interface PBXNativeTarget {
|
||||
isa: 'PBXNativeTarget';
|
||||
buildConfigurationList: UUID;
|
||||
buildConfigurationList_comment: string;
|
||||
buildPhases: Array<{
|
||||
value: UUID;
|
||||
comment: string;
|
||||
}>;
|
||||
buildRules: [];
|
||||
dependencies: [];
|
||||
name: string;
|
||||
productName: string;
|
||||
productReference: UUID;
|
||||
productReference_comment: string;
|
||||
productType: string;
|
||||
}
|
||||
|
||||
interface PBXBuildFile {
|
||||
isa: 'PBXBuildFile';
|
||||
fileRef: UUID;
|
||||
// "AppDelegate.m"
|
||||
fileRef_comment: string;
|
||||
}
|
||||
|
||||
interface XCConfigurationList {
|
||||
isa: 'XCConfigurationList';
|
||||
buildConfigurations: Array<{
|
||||
value: UUID;
|
||||
comment: string | 'Release' | 'Debug';
|
||||
}>;
|
||||
defaultConfigurationIsVisible: number;
|
||||
defaultConfigurationName: string;
|
||||
}
|
||||
|
||||
interface XCBuildConfiguration {
|
||||
isa: 'XCBuildConfiguration';
|
||||
baseConfigurationReference: UUID;
|
||||
baseConfigurationReference_comment: string;
|
||||
buildSettings: Record<string, string | undefined | number | Array<unknown>> & {
|
||||
// '"$(TARGET_NAME)"',
|
||||
PRODUCT_NAME?: string;
|
||||
// '"io.expo.demo.$(PRODUCT_NAME:rfc1034identifier)"',
|
||||
PRODUCT_BUNDLE_IDENTIFIER?: string;
|
||||
PROVISIONING_PROFILE_SPECIFIER?: string;
|
||||
// '"$(BUILT_PRODUCTS_DIR)/rni.app/rni"'
|
||||
TEST_HOST?: any;
|
||||
DEVELOPMENT_TEAM?: string;
|
||||
CODE_SIGN_IDENTITY?: string;
|
||||
CODE_SIGN_STYLE?: string;
|
||||
// '"$(TEST_HOST)"'
|
||||
BUNDLE_LOADER?: string;
|
||||
GCC_PREPROCESSOR_DEFINITIONS?: Array<unknown>;
|
||||
INFOPLIST_FILE?: string;
|
||||
IPHONEOS_DEPLOYMENT_TARGET?: string;
|
||||
LD_RUNPATH_SEARCH_PATHS?: string;
|
||||
OTHER_LDFLAGS?: Array<unknown>;
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME?: string;
|
||||
ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME?: string;
|
||||
CLANG_ANALYZER_NONNULL?: string;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS?: string;
|
||||
CLANG_WARN_INFINITE_RECURSION?: string;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE?: string;
|
||||
DEBUG_INFORMATION_FORMAT?: string;
|
||||
ENABLE_TESTABILITY?: string;
|
||||
GCC_NO_COMMON_BLOCKS?: string;
|
||||
// 'appletvos'
|
||||
SDKROOT?: string;
|
||||
TARGETED_DEVICE_FAMILY?: number | string;
|
||||
// '10.0'
|
||||
TVOS_DEPLOYMENT_TARGET?: string;
|
||||
};
|
||||
name: string;
|
||||
}
|
||||
|
||||
type ProductType =
|
||||
| 'com.apple.product-type.application'
|
||||
| 'com.apple.product-type.app-extension'
|
||||
| 'com.apple.product-type.bundle'
|
||||
| 'com.apple.product-type.tool'
|
||||
| 'com.apple.product-type.library.dynamic'
|
||||
| 'com.apple.product-type.framework'
|
||||
| 'com.apple.product-type.library.static'
|
||||
| 'com.apple.product-type.bundle.unit-test'
|
||||
| 'com.apple.product-type.application.watchapp'
|
||||
| 'com.apple.product-type.application.watchapp2'
|
||||
| 'com.apple.product-type.watchkit-extension'
|
||||
| 'com.apple.product-type.watchkit2-extension';
|
||||
|
||||
interface PBXGroup {
|
||||
isa: 'PBXGroup';
|
||||
children: Array<{
|
||||
value: UUID;
|
||||
comment?: string;
|
||||
}>;
|
||||
name: string;
|
||||
path?: string;
|
||||
sourceTree: '"<group>"' | unknown;
|
||||
}
|
||||
|
||||
export class XcodeProject {
|
||||
|
||||
/**
|
||||
* `.pbxproj` file path.
|
||||
*/
|
||||
public filepath: string;
|
||||
|
||||
// Ex: '$(TARGET_NAME)'
|
||||
public productName: string;
|
||||
|
||||
public hash: {
|
||||
headComment: string;
|
||||
project: {
|
||||
archiveVersion: number;
|
||||
objectVersion: number;
|
||||
objects: {
|
||||
[T in XCObjectType]: Record<
|
||||
string,
|
||||
{
|
||||
isa: T;
|
||||
name: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
>;
|
||||
};
|
||||
rootObject: string;
|
||||
rootObject_comment: string;
|
||||
};
|
||||
};
|
||||
constructor(pbxprojPath: string);
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
//
|
||||
// `.pbxproj` related operation - starting & ending point.
|
||||
//
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* First step to be executed while working with `.pbxproj` file.
|
||||
*/
|
||||
public parse(callback?: (err: Error | null, results?: string) => void): this;
|
||||
|
||||
public parseSync(): void;
|
||||
|
||||
/**
|
||||
* @returns Content of .pbxproj file.
|
||||
*/
|
||||
public writeSync(options?: { omitEmptyValues?: boolean }): string;
|
||||
|
||||
public allUuids(): UUID[];
|
||||
public generateUuid(): UUID;
|
||||
|
||||
public addPluginFile(path: unknown, opt: unknown): unknown;
|
||||
public removePluginFile(path: unknown, opt: unknown): unknown;
|
||||
public addProductFile(targetPath: unknown, opt: unknown): unknown;
|
||||
public removeProductFile(path: unknown, opt: unknown): unknown;
|
||||
public addSourceFile(path: string, opt: unknown, group: string): unknown;
|
||||
public removeSourceFile(path: string, opt: unknown, group: string): unknown;
|
||||
public addHeaderFile(path: string, opt: unknown, group: string): unknown;
|
||||
public removeHeaderFile(path: string, opt: unknown, group: string): unknown;
|
||||
public addResourceFile(path: string, opt: unknown, group: string): unknown;
|
||||
public removeResourceFile(path: string, opt: unknown, group: string): unknown;
|
||||
public addFramework(fpath: unknown, opt: unknown): unknown;
|
||||
public removeFramework(fpath: unknown, opt: unknown): unknown;
|
||||
public addCopyfile(fpath: unknown, opt: unknown): unknown;
|
||||
public pbxCopyfilesBuildPhaseObj(target: unknown): unknown;
|
||||
public addToPbxCopyfilesBuildPhase(file: unknown): void;
|
||||
public removeCopyfile(fpath: unknown, opt: unknown): unknown;
|
||||
public removeFromPbxCopyfilesBuildPhase(file: unknown): void;
|
||||
public addStaticLibrary(path: unknown, opt: unknown): unknown;
|
||||
/**
|
||||
* Adds to `PBXBuildFile` section
|
||||
*/
|
||||
public addToPbxBuildFileSection(file: PBXFile): void;
|
||||
public removeFromPbxBuildFileSection(file: unknown): void;
|
||||
public addPbxGroup(
|
||||
filePathsArray: string[],
|
||||
name: string,
|
||||
path: string,
|
||||
sourceTree?: string,
|
||||
): { uuid: UUID; pbxGroup: PBXGroup };
|
||||
public removePbxGroup(groupName: unknown): void;
|
||||
public addToPbxProjectSection(target: unknown): void;
|
||||
public addToPbxNativeTargetSection(target: unknown): void;
|
||||
public addToPbxFileReferenceSection(file: any): void;
|
||||
public removeFromPbxFileReferenceSection(file: unknown): unknown;
|
||||
public addToXcVersionGroupSection(file: unknown): void;
|
||||
public addToPluginsPbxGroup(file: unknown): void;
|
||||
public removeFromPluginsPbxGroup(file: unknown): unknown;
|
||||
public addToResourcesPbxGroup(file: unknown): void;
|
||||
public removeFromResourcesPbxGroup(file: unknown): unknown;
|
||||
public addToFrameworksPbxGroup(file: unknown): void;
|
||||
public removeFromFrameworksPbxGroup(file: unknown): unknown;
|
||||
public addToPbxEmbedFrameworksBuildPhase(file: unknown): void;
|
||||
public removeFromPbxEmbedFrameworksBuildPhase(file: unknown): void;
|
||||
public addToProductsPbxGroup(file: unknown): void;
|
||||
public removeFromProductsPbxGroup(file: unknown): unknown;
|
||||
public addToPbxSourcesBuildPhase(file: unknown): void;
|
||||
public removeFromPbxSourcesBuildPhase(file: unknown): void;
|
||||
/**
|
||||
* Adds to PBXResourcesBuildPhase` section
|
||||
* @param resourcesBuildPhaseSectionKey Because there's might more than one `Resources` build phase we need to ensure file is placed under correct one.
|
||||
*/
|
||||
public addToPbxResourcesBuildPhase(file: PBXFile): void;
|
||||
public removeFromPbxResourcesBuildPhase(file: unknown): void;
|
||||
public addToPbxFrameworksBuildPhase(file: unknown): void;
|
||||
public removeFromPbxFrameworksBuildPhase(file: unknown): void;
|
||||
public addXCConfigurationList(
|
||||
configurationObjectsArray: unknown,
|
||||
defaultConfigurationName: unknown,
|
||||
comment: unknown,
|
||||
): {
|
||||
uuid: unknown;
|
||||
xcConfigurationList: {
|
||||
isa: string;
|
||||
buildConfigurations: Array<unknown>;
|
||||
defaultConfigurationIsVisible: number;
|
||||
defaultConfigurationName: unknown;
|
||||
};
|
||||
};
|
||||
public addTargetDependency(
|
||||
target: unknown,
|
||||
dependencyTargets: unknown,
|
||||
): {
|
||||
uuid: unknown;
|
||||
target: unknown;
|
||||
};
|
||||
public addBuildPhase(
|
||||
filePathsArray: unknown,
|
||||
buildPhaseType: unknown,
|
||||
comment: unknown,
|
||||
target: unknown,
|
||||
optionsOrFolderType: unknown,
|
||||
subfolderPath: unknown,
|
||||
): {
|
||||
uuid: unknown;
|
||||
buildPhase: {
|
||||
isa: unknown;
|
||||
buildActionMask: number;
|
||||
files: Array<unknown>;
|
||||
runOnlyForDeploymentPostprocessing: number;
|
||||
};
|
||||
};
|
||||
/**
|
||||
* Retrieves main part describing PBXProjects that are available in `.pbxproj` file.
|
||||
*/
|
||||
public pbxProjectSection(): Record<UUID, PBXProject> & Record<UUIDComment, string>;
|
||||
public pbxBuildFileSection(): Record<UUID, PBXBuildFile> & Record<UUIDComment, string>;
|
||||
public pbxXCBuildConfigurationSection(): Record<UUID, XCBuildConfiguration> &
|
||||
Record<UUIDComment, string>;
|
||||
public pbxFileReferenceSection(): Record<UUID, PBXFile> & Record<UUIDComment, string>;
|
||||
public pbxNativeTargetSection(): Record<UUID, PBXNativeTarget> & Record<UUIDComment, string>;
|
||||
public xcVersionGroupSection(): unknown;
|
||||
public pbxXCConfigurationList(): Record<UUID, XCConfigurationList> & Record<UUIDComment, string>;
|
||||
public pbxGroupByName(name: string): PBXGroup | undefined;
|
||||
/**
|
||||
* @param targetName in most cases it's the name of the application
|
||||
*/
|
||||
public pbxTargetByName(targetName: string): PBXNativeTarget | undefined;
|
||||
public findTargetKey(name: string): string;
|
||||
public pbxItemByComment(name: string, pbxSectionName: unknown): unknown;
|
||||
public pbxSourcesBuildPhaseObj(target: unknown): unknown;
|
||||
public pbxResourcesBuildPhaseObj(target: unknown): unknown;
|
||||
public pbxFrameworksBuildPhaseObj(target: unknown): unknown;
|
||||
public pbxEmbedFrameworksBuildPhaseObj(target: unknown): unknown;
|
||||
public buildPhase(group: unknown, target: unknown): string;
|
||||
public buildPhaseObject(name: string, group: unknown, target: unknown): unknown;
|
||||
public addBuildProperty(prop: unknown, value: unknown, build_name: unknown): void;
|
||||
public removeBuildProperty(prop: unknown, build_name: unknown): void;
|
||||
public updateBuildProperty(prop: string, value: unknown, build: string): void;
|
||||
public updateProductName(name: string): void;
|
||||
public removeFromFrameworkSearchPaths(file: unknown): void;
|
||||
public addToFrameworkSearchPaths(file: unknown): void;
|
||||
public removeFromLibrarySearchPaths(file: unknown): void;
|
||||
public addToLibrarySearchPaths(file: unknown): void;
|
||||
public removeFromHeaderSearchPaths(file: unknown): void;
|
||||
public addToHeaderSearchPaths(file: unknown): void;
|
||||
public addToOtherLinkerFlags(flag: unknown): void;
|
||||
public removeFromOtherLinkerFlags(flag: unknown): void;
|
||||
public addToBuildSettings(buildSetting: unknown, value: unknown): void;
|
||||
public removeFromBuildSettings(buildSetting: unknown): void;
|
||||
/**
|
||||
* Checks whether there is a file with given `filePath` in the project.
|
||||
*/
|
||||
public hasFile(filePath: string): PBXFile | false;
|
||||
public addTarget(
|
||||
name: unknown,
|
||||
type: unknown,
|
||||
subfolder: unknown,
|
||||
): {
|
||||
uuid: unknown;
|
||||
pbxNativeTarget: {
|
||||
isa: string;
|
||||
name: string;
|
||||
productName: string;
|
||||
productReference: unknown;
|
||||
productType: string;
|
||||
buildConfigurationList: unknown;
|
||||
buildPhases: Array<unknown>;
|
||||
buildRules: Array<unknown>;
|
||||
dependencies: Array<unknown>;
|
||||
};
|
||||
};
|
||||
/**
|
||||
* Get First PBXProject that can be found in `.pbxproj` file.
|
||||
*/
|
||||
public getFirstProject(): { uuid: UUID; firstProject: PBXProject };
|
||||
public getFirstTarget(): {
|
||||
uuid: UUID;
|
||||
firstTarget: unknown;
|
||||
};
|
||||
/**
|
||||
* Retrieves PBXNativeTarget by the type
|
||||
*/
|
||||
public getTarget(productType: ProductType): { uuid: UUID; target: PBXNativeTarget } | null;
|
||||
public addToPbxGroupType(file: unknown, groupKey: unknown, groupType: unknown): void;
|
||||
public addToPbxVariantGroup(file: unknown, groupKey: unknown): void;
|
||||
public addToPbxGroup(file: PBXFile, groupKey: UUID): void;
|
||||
public pbxCreateGroupWithType(name: unknown, pathName: unknown, groupType: unknown): unknown;
|
||||
public pbxCreateVariantGroup(name: unknown): unknown;
|
||||
public pbxCreateGroup(name: string, pathName: string): UUID;
|
||||
public removeFromPbxGroupAndType(file: unknown, groupKey: unknown, groupType: unknown): void;
|
||||
public removeFromPbxGroup(file: unknown, groupKey: unknown): void;
|
||||
public removeFromPbxVariantGroup(file: unknown, groupKey: unknown): void;
|
||||
public getPBXGroupByKeyAndType(key: unknown, groupType: unknown): unknown;
|
||||
/**
|
||||
* @param groupKey UUID.
|
||||
*/
|
||||
public getPBXGroupByKey(groupKey: string): PBXGroup | undefined;
|
||||
public getPBXVariantGroupByKey(key: unknown): unknown;
|
||||
public findPBXGroupKeyAndType(criteria: unknown, groupType: unknown): string;
|
||||
/**
|
||||
* @param criteria Params that should be used to locate desired PBXGroup.
|
||||
*/
|
||||
public findPBXGroupKey(criteria: { name?: string; path?: string }): UUID | undefined;
|
||||
public findPBXVariantGroupKey(criteria: unknown): string;
|
||||
public addLocalizationVariantGroup(
|
||||
name: unknown,
|
||||
): {
|
||||
uuid: unknown;
|
||||
fileRef: unknown;
|
||||
basename: unknown;
|
||||
};
|
||||
public addKnownRegion(name: string): void;
|
||||
public removeKnownRegion(name: string): void;
|
||||
public hasKnownRegion(name: string): boolean;
|
||||
public getPBXObject(name: string): unknown;
|
||||
/**
|
||||
* - creates `PBXFile`
|
||||
* - adds to `PBXFileReference` section
|
||||
* - adds to `PBXGroup` or `PBXVariantGroup` if applicable
|
||||
* @returns `null` if file is already in `pbxproj`.
|
||||
*/
|
||||
public addFile(
|
||||
path: string,
|
||||
group?: string,
|
||||
opt?: {
|
||||
plugin?: string;
|
||||
target?: string;
|
||||
variantGroup?: string;
|
||||
lastKnownFileType?: string;
|
||||
defaultEncoding?: 4;
|
||||
customFramework?: true;
|
||||
explicitFileType?: number;
|
||||
weak?: true;
|
||||
compilerFLags?: string;
|
||||
embed?: boolean;
|
||||
sign?: boolean;
|
||||
},
|
||||
): PBXFile | null;
|
||||
public removeFile(path: unknown, group: unknown, opt: unknown): unknown;
|
||||
public getBuildProperty(prop: unknown, build: unknown): unknown;
|
||||
public getBuildConfigByName(name: unknown): object;
|
||||
public addDataModelDocument(filePath: unknown, group: unknown, opt: unknown): unknown;
|
||||
public addTargetAttribute(prop: unknown, value: unknown, target: unknown): void;
|
||||
public removeTargetAttribute(prop: unknown, target: unknown): void;
|
||||
}
|
||||
|
||||
export function project(projectPath: string): XcodeProject;
|
||||
}
|
||||
|
||||
declare module 'xcode/lib/pbxFile' {
|
||||
export default class PBXFile implements pbxFile {
|
||||
public basename: string;
|
||||
public lastKnownFileType?: string;
|
||||
public group?: string;
|
||||
public path?: string;
|
||||
public fileEncoding?: number;
|
||||
public defaultEncoding?: number;
|
||||
public sourceTree: string;
|
||||
public includeInIndex?: number;
|
||||
public explicitFileType?: unknown;
|
||||
public settings?: object;
|
||||
public uuid?: string;
|
||||
public fileRef: string;
|
||||
public target?: string;
|
||||
constructor(file: string);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
import Redis from 'ioredis';
|
||||
import chunk from 'lodash/chunk';
|
||||
|
||||
import config from 'turtle/config';
|
||||
import { getRedisClient, RedisClient } from 'turtle/utils/redis';
|
||||
import { sendBuildStatusUpdate, sendCredentialsUpdate } from 'turtle/utils/www';
|
||||
|
||||
const RETRY_COUNT = 5;
|
||||
|
||||
type UpdateType = 'build' | 'credentials';
|
||||
interface IUpdate {
|
||||
type: UpdateType;
|
||||
buildId: string;
|
||||
updateBody: any;
|
||||
retried: number;
|
||||
}
|
||||
|
||||
export async function saveUpdateFailure(buildId: string, type: UpdateType, updateBody: object): Promise<void> {
|
||||
if (config.env === 'test') {
|
||||
return;
|
||||
}
|
||||
const redis = await getRedisClient(RedisClient.Configuration) as Redis.Redis;
|
||||
const update: IUpdate = {
|
||||
type,
|
||||
buildId,
|
||||
updateBody,
|
||||
retried: 0,
|
||||
};
|
||||
await redis.set(getFailedUpdateKey(buildId), JSON.stringify(update));
|
||||
}
|
||||
|
||||
export async function synchronizeFailedUpdates(intervalMs = 3 * 60 * 1000): Promise<NodeJS.Timeout> {
|
||||
try {
|
||||
await _synchronizeFailedUpdates();
|
||||
} catch (err) {
|
||||
// don't do anything
|
||||
}
|
||||
return setInterval(() => _synchronizeFailedUpdates(), intervalMs);
|
||||
}
|
||||
|
||||
async function _synchronizeFailedUpdates(): Promise<void> {
|
||||
const redis = await getRedisClient(RedisClient.Configuration) as Redis.Redis;
|
||||
const keys = await redis.keys(getFailedUpdateKey('*'));
|
||||
const keyChunks = chunk(keys, 20);
|
||||
for (const keyChunk of keyChunks) {
|
||||
await Promise.all(keyChunk.map((key: string) => resendUpdateForKey(key)));
|
||||
}
|
||||
}
|
||||
|
||||
async function resendUpdateForKey(key: string): Promise<void> {
|
||||
try {
|
||||
const redis = await getRedisClient(RedisClient.Configuration) as Redis.Redis;
|
||||
const updateRaw = await redis.get(key);
|
||||
if (updateRaw) {
|
||||
const update = JSON.parse(updateRaw) as IUpdate;
|
||||
try {
|
||||
await resendUpdateToWWW(update);
|
||||
await redis.del(key);
|
||||
} catch (err) {
|
||||
update.retried += 1;
|
||||
if (update.retried >= RETRY_COUNT) {
|
||||
await redis.del(key);
|
||||
} else {
|
||||
await redis.set(key, JSON.stringify(update));
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
// don't do anything
|
||||
}
|
||||
}
|
||||
|
||||
async function resendUpdateToWWW(update: IUpdate): Promise<void> {
|
||||
const { type, buildId, updateBody } = update;
|
||||
if (type === 'build') {
|
||||
await sendBuildStatusUpdate(buildId, updateBody);
|
||||
} else if (type === 'credentials') {
|
||||
const { platform, ...rest } = updateBody;
|
||||
await sendCredentialsUpdate(buildId, platform, rest);
|
||||
}
|
||||
}
|
||||
|
||||
const getFailedUpdateKey = (buildId: string) => `${config.deploymentEnv}:failed-update:${buildId}`;
|
||||
@@ -0,0 +1,164 @@
|
||||
import { EventEmitter } from 'events';
|
||||
import Redis from 'ioredis';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
import { getRedisClient, RedisClient } from 'turtle/utils/redis';
|
||||
|
||||
interface ILeaderParams {
|
||||
key: string;
|
||||
id?: string;
|
||||
ttl?: number;
|
||||
wait?: number;
|
||||
}
|
||||
|
||||
const RENEW_LEADER_SCRIPT = `
|
||||
local leader_key = KEYS[1]
|
||||
local leader_id = KEYS[2]
|
||||
local leader_ttl = KEYS[3]
|
||||
|
||||
local current_leader
|
||||
|
||||
current_leader = redis.call("get", leader_key)
|
||||
if (current_leader == leader_id) then
|
||||
redis.call("pexpire", leader_key, leader_ttl)
|
||||
return "ok"
|
||||
else
|
||||
return "you_are_not_the_leader"
|
||||
end
|
||||
`;
|
||||
|
||||
const STOP_BEING_LEADER_SCRIPT = `
|
||||
local leader_key = KEYS[1]
|
||||
local leader_id = KEYS[2]
|
||||
|
||||
local current_leader
|
||||
|
||||
current_leader = redis.call("get", leader_key)
|
||||
if (current_leader == leader_id) then
|
||||
redis.call("del", leader_key)
|
||||
return "ok"
|
||||
else
|
||||
return "you_are_not_the_leader"
|
||||
end
|
||||
`;
|
||||
|
||||
interface IRedisCustom extends Redis.Redis {
|
||||
renewLeader(key: string, id: string, ttl: number): Promise<string>;
|
||||
stopBeingLeader(key: string, id: string): Promise<string>;
|
||||
}
|
||||
|
||||
enum LeaderEvent {
|
||||
elected = 'elected',
|
||||
revoked = 'revoked',
|
||||
error = 'error',
|
||||
}
|
||||
|
||||
class Leader extends EventEmitter {
|
||||
private key: string;
|
||||
private id: string;
|
||||
private ttl: number;
|
||||
private wait: number;
|
||||
private waitId?: NodeJS.Timeout;
|
||||
private redisClientValue?: IRedisCustom;
|
||||
private renewId?: NodeJS.Timeout;
|
||||
private started = false;
|
||||
|
||||
constructor({ key, id, ttl = 30, wait = 1000 }: ILeaderParams) {
|
||||
super();
|
||||
|
||||
this.key = key;
|
||||
this.id = id || uuid();
|
||||
this.ttl = ttl * 1000;
|
||||
this.wait = wait;
|
||||
}
|
||||
public async elect(): Promise<void> {
|
||||
if (!this.redisClientValue) {
|
||||
await this.init();
|
||||
}
|
||||
|
||||
if (!this.started) {
|
||||
this.started = true;
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await this.redisClient.set(this.key, this.id, 'PX', this.ttl, 'NX');
|
||||
if (res === null) {
|
||||
this.waitId = setTimeout(() => {
|
||||
this.elect();
|
||||
}, this.wait);
|
||||
} else {
|
||||
this.emit(LeaderEvent.elected);
|
||||
this.renewId = setInterval(() => {
|
||||
this.renew();
|
||||
}, this.ttl / 2);
|
||||
}
|
||||
} catch (err) {
|
||||
this.emit(LeaderEvent.error, err);
|
||||
}
|
||||
}
|
||||
|
||||
public async stop(): Promise<void> {
|
||||
if (!this.started) {
|
||||
throw new Error('You should start the election first!');
|
||||
}
|
||||
try {
|
||||
const res = await this.redisClient.stopBeingLeader(this.key, this.id);
|
||||
if (res === 'ok') {
|
||||
this.emit(LeaderEvent.revoked);
|
||||
}
|
||||
if (this.renewId) {
|
||||
clearInterval(this.renewId);
|
||||
}
|
||||
if (this.waitId) {
|
||||
clearTimeout(this.waitId);
|
||||
}
|
||||
this.redisClientValue?.disconnect();
|
||||
this.redisClientValue = undefined;
|
||||
} catch (err) {
|
||||
this.emit(LeaderEvent.error, err);
|
||||
}
|
||||
}
|
||||
|
||||
private get redisClient(): IRedisCustom {
|
||||
if (!this.redisClientValue) {
|
||||
throw new Error('Please run the init method first!');
|
||||
}
|
||||
return this.redisClientValue as IRedisCustom;
|
||||
}
|
||||
|
||||
private async init(): Promise<void> {
|
||||
if (this.redisClientValue) {
|
||||
return;
|
||||
}
|
||||
this.redisClientValue = await getRedisClient(RedisClient.Configuration);
|
||||
const redisClient = this.redisClient as IRedisCustom;
|
||||
redisClient.defineCommand('renewLeader', {
|
||||
numberOfKeys: 3,
|
||||
lua: RENEW_LEADER_SCRIPT,
|
||||
});
|
||||
redisClient.defineCommand('stopBeingLeader', {
|
||||
numberOfKeys: 2,
|
||||
lua: STOP_BEING_LEADER_SCRIPT,
|
||||
});
|
||||
}
|
||||
|
||||
private async renew(): Promise<void> {
|
||||
try {
|
||||
const result = await this.redisClient.renewLeader(this.key, this.id, this.ttl);
|
||||
if (result === 'you_are_not_the_leader') {
|
||||
if (this.renewId) {
|
||||
clearInterval(this.renewId);
|
||||
}
|
||||
this.waitId = setTimeout(() => {
|
||||
this.elect();
|
||||
}, this.wait);
|
||||
this.emit(LeaderEvent.revoked);
|
||||
}
|
||||
} catch (err) {
|
||||
this.emit(LeaderEvent.error, err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default Leader;
|
||||
export { LeaderEvent };
|
||||
+15
-6
@@ -12,18 +12,27 @@ export enum RedisClient {
|
||||
Configuration = 'REDIS_CLIENT_CONFIGURATION',
|
||||
}
|
||||
|
||||
interface IClientConfig { [key: string]: string; }
|
||||
interface IClientConfig {
|
||||
[key: string]: {
|
||||
url: string;
|
||||
caCertificate?: string;
|
||||
};
|
||||
}
|
||||
|
||||
const clientToUrl: IClientConfig = {
|
||||
[RedisClient.Subscriber]: config.redis.url,
|
||||
[RedisClient.Default]: config.redis.url,
|
||||
[RedisClient.Configuration]: config.redis.configUrl,
|
||||
const clientConfigs: IClientConfig = {
|
||||
[RedisClient.Subscriber]: config.redis.main,
|
||||
[RedisClient.Default]: config.redis.main,
|
||||
[RedisClient.Configuration]: config.redis.turtle,
|
||||
};
|
||||
|
||||
export function connect(timeoutMs: number, type: string): Promise<any> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const redisClient = new Redis(clientToUrl[type], {
|
||||
const { url, caCertificate } = clientConfigs[type];
|
||||
const redisClient = new Redis(url, {
|
||||
maxRetriesPerRequest: 2,
|
||||
...caCertificate && {
|
||||
tls: { ca: caCertificate },
|
||||
},
|
||||
});
|
||||
const timer = setTimeout(() => reject(new Error('Timeout at connecting to Redis')), timeoutMs);
|
||||
redisClient.on('ready', () => {
|
||||
|
||||
@@ -10,7 +10,7 @@ async function setSupportedSdkVersions(platform: string, sdkVersions: string[])
|
||||
method: 'POST',
|
||||
uri: href,
|
||||
headers: {
|
||||
'secret-token': config.www.sdkVersionsSecretToken,
|
||||
'secret-token': config.www.secretToken,
|
||||
},
|
||||
body: {
|
||||
platform,
|
||||
@@ -26,7 +26,7 @@ async function setTurtleVersion(version: string) {
|
||||
method: 'POST',
|
||||
uri: href,
|
||||
headers: {
|
||||
'secret-token': config.www.sdkVersionsSecretToken,
|
||||
'secret-token': config.www.secretToken,
|
||||
},
|
||||
body: {
|
||||
version,
|
||||
@@ -38,9 +38,9 @@ async function setTurtleVersion(version: string) {
|
||||
function resolveWWWEndpoint() {
|
||||
const currentEnv = config.deploymentEnv;
|
||||
if (currentEnv === 'production') {
|
||||
return 'https://expo.io';
|
||||
return 'https://exp.host';
|
||||
} else if (currentEnv === 'staging') {
|
||||
return 'https://staging.expo.io';
|
||||
return 'https://staging.exp.host';
|
||||
} else {
|
||||
return 'http://127.0.0.1:3000';
|
||||
}
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import { URL } from 'url';
|
||||
|
||||
import request from 'request-promise';
|
||||
|
||||
import config from 'turtle/config';
|
||||
import { saveUpdateFailure } from 'turtle/updatesManager';
|
||||
|
||||
async function sendBuildStatusUpdate(buildId: string, updateParams: object) {
|
||||
const { href } = new URL(`/--/api/v2/turtle-legacy-builds/${buildId}`, resolveWWWEndpoint());
|
||||
try {
|
||||
await request({
|
||||
method: 'PUT',
|
||||
uri: href,
|
||||
headers: {
|
||||
'secret-token': config.www.secretToken,
|
||||
},
|
||||
body: updateParams,
|
||||
json: true,
|
||||
});
|
||||
} catch (err) {
|
||||
await saveUpdateFailure(buildId, 'build', updateParams);
|
||||
}
|
||||
}
|
||||
|
||||
async function sendCredentialsUpdate(buildId: string, platform: 'android' | 'ios', updateParams: object) {
|
||||
const { href } = new URL(`/--/api/v2/turtle-legacy-builds/${buildId}/credentials`, resolveWWWEndpoint());
|
||||
try {
|
||||
await request({
|
||||
method: 'PUT',
|
||||
uri: href,
|
||||
headers: {
|
||||
'secret-token': config.www.secretToken,
|
||||
},
|
||||
body: {
|
||||
platform,
|
||||
...updateParams,
|
||||
},
|
||||
json: true,
|
||||
});
|
||||
} catch (err) {
|
||||
await saveUpdateFailure(buildId, 'credentials', updateParams);
|
||||
}
|
||||
}
|
||||
|
||||
function resolveWWWEndpoint() {
|
||||
const currentEnv = config.deploymentEnv;
|
||||
if (currentEnv === 'production') {
|
||||
return 'https://exp.host';
|
||||
} else if (currentEnv === 'staging') {
|
||||
return 'https://staging.exp.host';
|
||||
} else {
|
||||
return 'http://127.0.0.1:3000';
|
||||
}
|
||||
}
|
||||
|
||||
export { sendBuildStatusUpdate, sendCredentialsUpdate };
|
||||
@@ -0,0 +1,143 @@
|
||||
import fs from 'fs-extra';
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
|
||||
/*
|
||||
A Cacher is used to wrap a fallible or expensive function and to memoize its results on disk
|
||||
in case it either fails or we don't need fresh results very often. It stores objects in JSON, and
|
||||
parses JSON from disk when returning an object.
|
||||
|
||||
It's constructed with a "refresher" callback which will be called for the results, a filename to use
|
||||
for the cache, and an optional TTL and boostrap file. The TTL (in milliseconds) can be used to speed
|
||||
up slow calls from the cache (for example checking npm published versions can be very slow). The
|
||||
bootstrap file can be used to "seed" the cache with a particular value stored in a file.
|
||||
|
||||
If there is a problem calling the refresher function or in performing the cache's disk I/O, errors
|
||||
will be stored in variables on the class. The only times Cacher will throw an exception are if it's
|
||||
not possible to create the cache directory (usually weird home directory permissions), or if getAsync()
|
||||
is called but no value can be provided. The latter will only occur if the refresher fails, no cache
|
||||
is available on disk (i.e. this is the first call or it has been recently cleared), and bootstrapping
|
||||
was not available (either a bootstrap file wasn't provided or reading/writing failed).
|
||||
|
||||
See src/__tests__/tools/FsCache-test.js for usage examples.
|
||||
*/
|
||||
class Cacher<T> {
|
||||
public refresher: () => Promise<T>;
|
||||
public filename: string;
|
||||
public bootstrapFile?: string;
|
||||
public ttlMilliseconds: number;
|
||||
|
||||
public readError?: any;
|
||||
public writeError?: any;
|
||||
|
||||
constructor(
|
||||
refresher: () => Promise<T>,
|
||||
filename: string,
|
||||
ttlMilliseconds?: number,
|
||||
bootstrapFile?: string,
|
||||
) {
|
||||
this.refresher = refresher;
|
||||
this.filename = path.join(getCacheDir(), filename);
|
||||
this.ttlMilliseconds = ttlMilliseconds || 0;
|
||||
this.bootstrapFile = bootstrapFile;
|
||||
}
|
||||
|
||||
public async getAsync(): Promise<T> {
|
||||
// Let user opt out of cache for debugging purposes
|
||||
if (process.env.SKIP_CACHE) {
|
||||
return await this.refresher();
|
||||
}
|
||||
|
||||
let mtime: Date;
|
||||
try {
|
||||
const stats = await fs.stat(this.filename);
|
||||
mtime = stats.mtime;
|
||||
} catch (e) {
|
||||
try {
|
||||
await fs.mkdirp(getCacheDir());
|
||||
|
||||
if (this.bootstrapFile) {
|
||||
const bootstrapContents = (
|
||||
await fs.readFile(this.bootstrapFile)
|
||||
).toString();
|
||||
|
||||
await fs.writeFile(this.filename, bootstrapContents, 'utf8');
|
||||
}
|
||||
} catch (e) {
|
||||
// intentional no-op
|
||||
}
|
||||
mtime = new Date(1989, 10, 19);
|
||||
}
|
||||
|
||||
let fromCache: T | null = null;
|
||||
let failedRefresh = null;
|
||||
|
||||
// if mtime + ttl >= now, attempt to fetch the value, otherwise read from disk
|
||||
// alternatively, if ttlMilliseconds is 0 we also update every time, regardless of the times.
|
||||
// this is a workaround for the issue described in https://github.com/expo/expo-cli/issues/1683
|
||||
if (
|
||||
this.ttlMilliseconds === 0 ||
|
||||
new Date().getTime() - mtime.getTime() > this.ttlMilliseconds
|
||||
) {
|
||||
try {
|
||||
fromCache = await this.refresher();
|
||||
|
||||
try {
|
||||
await fs.writeFile(this.filename, JSON.stringify(fromCache), 'utf8');
|
||||
} catch (e) {
|
||||
this.writeError = e;
|
||||
// do nothing, if the refresh succeeded it'll be returned, if the persist failed we don't care
|
||||
}
|
||||
} catch (e) {
|
||||
failedRefresh = e;
|
||||
}
|
||||
}
|
||||
|
||||
if (!fromCache) {
|
||||
try {
|
||||
fromCache = JSON.parse(await fs.readFile(this.filename, 'utf8'));
|
||||
} catch (e) {
|
||||
this.readError = e;
|
||||
// if this fails then we've exhausted our options and it should remain null
|
||||
}
|
||||
}
|
||||
|
||||
if (fromCache) {
|
||||
return fromCache;
|
||||
} else {
|
||||
if (failedRefresh) {
|
||||
throw new Error(
|
||||
`Unable to perform cache refresh for ${this.filename}: ${failedRefresh}`,
|
||||
);
|
||||
} else {
|
||||
throw new Error(
|
||||
`Unable to read ${this.filename}. ${this.readError || ''}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async clearAsync(): Promise<void> {
|
||||
try {
|
||||
await fs.unlink(this.filename);
|
||||
} catch (e) {
|
||||
this.writeError = e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getCacheDir(): string {
|
||||
const homeDir = os.homedir();
|
||||
if (process.env.XDG_CACHE_HOME) {
|
||||
return process.env.XDG_CACHE_HOME;
|
||||
} else if (process.platform === 'win32') {
|
||||
return path.join(homeDir, 'AppData', 'Local', 'Expo');
|
||||
} else if (process.platform === 'darwin') {
|
||||
// too many mac users have broken permissions on their ~/.cache directory
|
||||
return path.join(homeDir, '.expo', 'cache');
|
||||
} else {
|
||||
return path.join(homeDir, '.cache', 'expo');
|
||||
}
|
||||
}
|
||||
|
||||
export { Cacher, getCacheDir };
|
||||
@@ -0,0 +1,103 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import probeImageSize from 'probe-image-size';
|
||||
|
||||
import { spawnAsyncThrowError } from './detach/ExponentTools';
|
||||
import LoggerDetach from './detach/Logger';
|
||||
|
||||
/**
|
||||
* @param {string} projectDirname
|
||||
* @param {string} basename
|
||||
* @returns {} { width: number, height: number } image dimensions or null
|
||||
*/
|
||||
async function getImageDimensionsAsync(
|
||||
projectDirname: string,
|
||||
basename: string,
|
||||
): Promise<{ width?: number; height?: number } | null> {
|
||||
return await customGetImageDimensionsAsync(projectDirname, basename).catch(
|
||||
() => null,
|
||||
);
|
||||
}
|
||||
|
||||
async function customGetImageDimensionsWithImageProbeAsync(
|
||||
projectDirname: string,
|
||||
basename: string,
|
||||
): Promise<{ width: number; height: number }> {
|
||||
const imagePath = path.resolve(projectDirname, basename);
|
||||
const readStream = fs.createReadStream(imagePath);
|
||||
const { width, height } = await probeImageSize(readStream);
|
||||
readStream.destroy();
|
||||
return { width, height };
|
||||
}
|
||||
|
||||
let hasWarned = false;
|
||||
async function resizeImageAsync(
|
||||
iconSizePx: number,
|
||||
iconFilename: string,
|
||||
destinationIconPath: string,
|
||||
) {
|
||||
if (
|
||||
process.platform !== 'darwin' &&
|
||||
customResizeImageAsync === customResizeImageWithSipsAsync &&
|
||||
!hasWarned
|
||||
) {
|
||||
LoggerDetach.warn('`sips` utility may or may not work outside of macOS');
|
||||
hasWarned = true;
|
||||
}
|
||||
return customResizeImageAsync(iconSizePx, iconFilename, destinationIconPath);
|
||||
}
|
||||
|
||||
async function customResizeImageWithSipsAsync(
|
||||
iconSizePx: number,
|
||||
iconFilename: string,
|
||||
destinationIconPath: string,
|
||||
) {
|
||||
return spawnAsyncThrowError(
|
||||
'sips',
|
||||
['-Z', iconSizePx.toFixed(), iconFilename],
|
||||
{
|
||||
stdio: ['ignore', 'ignore', 'inherit'], // only stderr
|
||||
cwd: destinationIconPath,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// Allow us to swap out the default implementations of image functions
|
||||
let customResizeImageAsync = customResizeImageWithSipsAsync;
|
||||
let customGetImageDimensionsAsync: (
|
||||
dirname: string,
|
||||
filename: string,
|
||||
) => Promise<{
|
||||
width?: number;
|
||||
height?: number;
|
||||
} | null> = customGetImageDimensionsWithImageProbeAsync;
|
||||
|
||||
// Allow users to provide an alternate implementation for our image resize function.
|
||||
// This is used internally in order to use sharp instead of sips in standalone builder.
|
||||
function setResizeImageFunction(
|
||||
fn: (
|
||||
iconSizePx: number,
|
||||
iconFilename: string,
|
||||
destinationIconPath: string,
|
||||
) => Promise<any>,
|
||||
) {
|
||||
customResizeImageAsync = fn;
|
||||
}
|
||||
|
||||
// Allow users to provide an alternate implementation for our image dimensions function.
|
||||
// This is used internally in order to use sharp instead of sips in standalone builder.
|
||||
function setGetImageDimensionsFunction(
|
||||
fn: (
|
||||
dirname: string,
|
||||
filename: string,
|
||||
) => Promise<{ width?: number; height?: number } | null>,
|
||||
) {
|
||||
customGetImageDimensionsAsync = fn;
|
||||
}
|
||||
|
||||
export {
|
||||
resizeImageAsync,
|
||||
setResizeImageFunction,
|
||||
setGetImageDimensionsFunction,
|
||||
getImageDimensionsAsync,
|
||||
};
|
||||
@@ -0,0 +1,38 @@
|
||||
import latestVersionAsync from 'latest-version';
|
||||
import pTimeout from 'p-timeout';
|
||||
import npmPackageJson from 'package-json';
|
||||
import semver from 'semver';
|
||||
|
||||
import { Cacher } from './FsCache';
|
||||
|
||||
/** @deprecated just use the update-check npm package */
|
||||
function createModuleVersionChecker(name: string, currentVersion: string) {
|
||||
const UpdateCacher = new Cacher(
|
||||
async () => {
|
||||
const pkgJson = await pTimeout(
|
||||
npmPackageJson(name, { version: currentVersion }),
|
||||
2000,
|
||||
);
|
||||
return {
|
||||
latestVersion: await pTimeout(latestVersionAsync(name), 2000),
|
||||
deprecated: pkgJson.deprecated,
|
||||
};
|
||||
},
|
||||
`${name}-updates.json`,
|
||||
24 * 60 * 60 * 1000, // one day
|
||||
);
|
||||
|
||||
async function checkAsync() {
|
||||
const { latestVersion, deprecated } = await UpdateCacher.getAsync();
|
||||
return {
|
||||
updateIsAvailable: semver.gt(latestVersion, currentVersion),
|
||||
latest: latestVersion,
|
||||
current: currentVersion,
|
||||
deprecated,
|
||||
};
|
||||
}
|
||||
|
||||
return { checkAsync };
|
||||
}
|
||||
|
||||
export { createModuleVersionChecker };
|
||||
@@ -0,0 +1,24 @@
|
||||
export type ErrorCode = 'DIRECTORY_ALREADY_EXISTS' | 'INVALID_JSON';
|
||||
|
||||
const ERROR_PREFIX = 'Error: ';
|
||||
|
||||
export default class XDLError extends Error {
|
||||
public readonly name = 'XDLError';
|
||||
|
||||
public code: string;
|
||||
public isXDLError: true;
|
||||
|
||||
constructor(code: ErrorCode, message: string) {
|
||||
super('');
|
||||
|
||||
// If e.toString() was called to get `message` we don't want it to look
|
||||
// like "Error: Error:".
|
||||
if (message.startsWith(ERROR_PREFIX)) {
|
||||
message = message.substring(ERROR_PREFIX.length);
|
||||
}
|
||||
|
||||
this.message = message;
|
||||
this.code = code;
|
||||
this.isXDLError = true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,275 @@
|
||||
import fs from 'fs-extra';
|
||||
import { sync as globSync } from 'glob';
|
||||
import path from 'path';
|
||||
import logger from 'turtle/logger';
|
||||
|
||||
import { getImageDimensionsAsync, resizeImageAsync } from '../ImageUtils';
|
||||
import {
|
||||
regexFileAsync,
|
||||
saveImageToPathAsync,
|
||||
saveUrlToPathAsync,
|
||||
spawnAsyncThrowError,
|
||||
} from './ExponentTools';
|
||||
import {
|
||||
AnyStandaloneContext,
|
||||
IStandaloneContextDataUser,
|
||||
} from './StandaloneContext';
|
||||
|
||||
const iconScales = {
|
||||
mdpi: 1,
|
||||
hdpi: 1.5,
|
||||
xhdpi: 2,
|
||||
xxhdpi: 3,
|
||||
xxxhdpi: 4,
|
||||
};
|
||||
|
||||
async function regexFileInResSubfoldersAsync(
|
||||
oldText: string,
|
||||
newText: string,
|
||||
resDirPath: string,
|
||||
folderPrefix: string,
|
||||
folderSuffix: string,
|
||||
fileName: string,
|
||||
) {
|
||||
return Promise.all(
|
||||
Object.keys(iconScales).map(async (key) => {
|
||||
return regexFileAsync(
|
||||
oldText,
|
||||
newText,
|
||||
path.join(resDirPath, `${folderPrefix}${key}${folderSuffix}`, fileName),
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
let hasShownResizeErrorWindowsLinux = false;
|
||||
|
||||
async function resizeIconsAsync(
|
||||
context: AnyStandaloneContext,
|
||||
resPath: string,
|
||||
prefix: string,
|
||||
mdpiSize: number,
|
||||
filename: string,
|
||||
url: string,
|
||||
isDetached: boolean,
|
||||
) {
|
||||
const baseImagePath = path.join(resPath, filename);
|
||||
|
||||
try {
|
||||
if (isDetached) {
|
||||
const data = context.data as IStandaloneContextDataUser;
|
||||
await saveImageToPathAsync(data.projectPath, url, baseImagePath);
|
||||
} else {
|
||||
await saveUrlToPathAsync(url, baseImagePath);
|
||||
}
|
||||
} catch (e) {
|
||||
throw new Error(`Failed to save icon file to disk. (${e})`);
|
||||
}
|
||||
|
||||
await Promise.all(
|
||||
Object.entries(iconScales).map(async ([folderSuffix, iconScale]) => {
|
||||
// adaptive icons (mdpiSize 108) must be placed in a -v26 folder
|
||||
const subdirectoryName = `${prefix}${folderSuffix}${
|
||||
mdpiSize === 108 ? '-v26' : ''
|
||||
}`;
|
||||
const destinationPath = path.join(resPath, subdirectoryName);
|
||||
await spawnAsyncThrowError('/bin/cp', [baseImagePath, filename], {
|
||||
stdio: 'inherit',
|
||||
cwd: destinationPath,
|
||||
});
|
||||
|
||||
try {
|
||||
await resizeImageAsync(mdpiSize * iconScale, filename, destinationPath);
|
||||
} catch (e) {
|
||||
// Turtle should be able to resize images, so if it fails we want it to throw.
|
||||
// However, `sips` does not exist on Windows or Linux machines, so we expect
|
||||
// resizing images to error on these OSes and want the detach process to continue anyway.
|
||||
if (isDetached) {
|
||||
if (!hasShownResizeErrorWindowsLinux) {
|
||||
logger.warn(
|
||||
'Failed to resize app icons. ' +
|
||||
'Your full size icon will be copied to all android/app/src/main/res directories. ' +
|
||||
'For best quality, we recommend providing downscaled versions.',
|
||||
);
|
||||
hasShownResizeErrorWindowsLinux = true;
|
||||
}
|
||||
} else {
|
||||
throw new Error(`Failed to resize image: ${filename}. ${e}`);
|
||||
}
|
||||
}
|
||||
|
||||
// reject non-square icons
|
||||
const dims = await getImageDimensionsAsync(destinationPath, filename);
|
||||
if (!dims) {
|
||||
// Again, only throw this error on Turtle -- we expect that this will fail
|
||||
// for some detach users but we don't want this to stop the whole process.
|
||||
if (!isDetached) {
|
||||
throw new Error(`Unable to read the dimensions of ${filename}`);
|
||||
}
|
||||
} else if (dims.width !== dims.height) {
|
||||
throw new Error(
|
||||
`Android icons must be square, the dimensions of ${filename} are ${dims}`,
|
||||
);
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
await spawnAsyncThrowError('/bin/rm', [baseImagePath]);
|
||||
}
|
||||
|
||||
async function createAndWriteIconsToPathAsync(
|
||||
context: AnyStandaloneContext,
|
||||
resPath: string,
|
||||
isDetached: boolean,
|
||||
) {
|
||||
const manifest = context.config; // manifest or app.json
|
||||
let iconUrl =
|
||||
manifest.android && manifest.android.iconUrl
|
||||
? manifest.android.iconUrl
|
||||
: manifest.iconUrl;
|
||||
let notificationIconUrl = manifest.notification
|
||||
? manifest.notification.iconUrl
|
||||
: null;
|
||||
|
||||
if (isDetached) {
|
||||
// manifest is actually just app.json in this case, so iconUrl fields don't exist
|
||||
iconUrl =
|
||||
manifest.android && manifest.android.icon
|
||||
? manifest.android.icon
|
||||
: manifest.icon;
|
||||
notificationIconUrl = manifest.notification
|
||||
? manifest.notification.icon
|
||||
: null;
|
||||
}
|
||||
|
||||
let iconBackgroundUrl;
|
||||
let iconBackgroundColor;
|
||||
let iconForegroundUrl;
|
||||
if (manifest.android && manifest.android.adaptiveIcon) {
|
||||
iconBackgroundColor = manifest.android.adaptiveIcon.backgroundColor;
|
||||
if (isDetached) {
|
||||
iconForegroundUrl = manifest.android.adaptiveIcon.foregroundImage;
|
||||
iconBackgroundUrl = manifest.android.adaptiveIcon.backgroundImage;
|
||||
} else {
|
||||
iconForegroundUrl = manifest.android.adaptiveIcon.foregroundImageUrl;
|
||||
iconBackgroundUrl = manifest.android.adaptiveIcon.backgroundImageUrl;
|
||||
}
|
||||
}
|
||||
|
||||
if (iconUrl || iconForegroundUrl) {
|
||||
// Android 7 and below icon
|
||||
if (iconUrl) {
|
||||
globSync('**/ic_launcher.png', {
|
||||
cwd: resPath,
|
||||
absolute: true,
|
||||
}).forEach((filePath) => {
|
||||
fs.removeSync(filePath);
|
||||
});
|
||||
|
||||
await resizeIconsAsync(
|
||||
context,
|
||||
resPath,
|
||||
'mipmap-',
|
||||
48,
|
||||
'ic_launcher.png',
|
||||
iconUrl,
|
||||
isDetached,
|
||||
);
|
||||
}
|
||||
|
||||
// Adaptive icon foreground image
|
||||
if (iconForegroundUrl) {
|
||||
globSync('**/ic_foreground.png', {
|
||||
cwd: resPath,
|
||||
absolute: true,
|
||||
}).forEach((filePath) => {
|
||||
fs.removeSync(filePath);
|
||||
});
|
||||
|
||||
await resizeIconsAsync(
|
||||
context,
|
||||
resPath,
|
||||
'mipmap-',
|
||||
108,
|
||||
'ic_foreground.png',
|
||||
iconForegroundUrl,
|
||||
isDetached,
|
||||
);
|
||||
} else {
|
||||
// the OS's default method of coercing normal app icons to adaptive
|
||||
// makes them look quite different from using an actual adaptive icon (with xml)
|
||||
// so we need to support falling back to the old version on Android 8
|
||||
globSync('**/mipmap-*-v26/*', {
|
||||
cwd: resPath,
|
||||
absolute: true,
|
||||
dot: true,
|
||||
}).forEach((filePath) => {
|
||||
fs.removeSync(filePath);
|
||||
});
|
||||
|
||||
try {
|
||||
globSync('**/mipmap-*-v26', {
|
||||
cwd: resPath,
|
||||
absolute: true,
|
||||
}).forEach((filePath) => {
|
||||
fs.rmdirSync(filePath);
|
||||
});
|
||||
} catch (e) {
|
||||
// we don't want the entire detach script to fail if node
|
||||
// can't remove the directories for whatever reason.
|
||||
// people can remove the directories themselves if they need
|
||||
// so just fail silently here
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Adaptive icon background image or color
|
||||
if (iconBackgroundUrl) {
|
||||
await resizeIconsAsync(
|
||||
context,
|
||||
resPath,
|
||||
'mipmap-',
|
||||
108,
|
||||
'ic_background.png',
|
||||
iconBackgroundUrl,
|
||||
isDetached,
|
||||
);
|
||||
|
||||
await regexFileInResSubfoldersAsync(
|
||||
'@color/iconBackground',
|
||||
'@mipmap/ic_background',
|
||||
resPath,
|
||||
'mipmap-',
|
||||
'-v26',
|
||||
'ic_launcher.xml',
|
||||
);
|
||||
} else if (iconBackgroundColor) {
|
||||
await regexFileAsync(
|
||||
'"iconBackground">#FFFFFF',
|
||||
`"iconBackground">${iconBackgroundColor}`,
|
||||
path.join(resPath, 'values', 'colors.xml'),
|
||||
);
|
||||
}
|
||||
|
||||
// Remove Expo client notification icon resources
|
||||
globSync('**/shell_notification_icon.png', {
|
||||
cwd: resPath,
|
||||
absolute: true,
|
||||
}).forEach((filePath) => {
|
||||
fs.removeSync(filePath);
|
||||
});
|
||||
|
||||
// Add provided notification icon resources, falling back
|
||||
// to the app icon if no `notification.icon` is provided
|
||||
await resizeIconsAsync(
|
||||
context,
|
||||
resPath,
|
||||
'drawable-',
|
||||
24,
|
||||
'shell_notification_icon.png',
|
||||
notificationIconUrl ?? iconUrl,
|
||||
isDetached,
|
||||
);
|
||||
}
|
||||
|
||||
export { createAndWriteIconsToPathAsync };
|
||||
@@ -0,0 +1,42 @@
|
||||
export default function renderIntentFilters(intentFilters: any) {
|
||||
// returns an array of <intent-filter> tags:
|
||||
// [
|
||||
// `<intent-filter>
|
||||
// <data android:scheme="exp"/>
|
||||
// <data android:scheme="exps"/>
|
||||
//
|
||||
// <action android:name="android.intent.action.VIEW"/>
|
||||
//
|
||||
// <category android:name="android.intent.category.DEFAULT"/>
|
||||
// <category android:name="android.intent.category.BROWSABLE"/>
|
||||
// </intent-filter>`,
|
||||
// ...
|
||||
// ]
|
||||
return intentFilters.map((intentFilter: any) => {
|
||||
const autoVerify = intentFilter.autoVerify ? ' android:autoVerify="true"' : '';
|
||||
|
||||
return `<intent-filter${autoVerify}>
|
||||
${renderIntentFilterData(intentFilter.data)}
|
||||
<action android:name="android.intent.action.${intentFilter.action}"/>
|
||||
${renderIntentFilterCategory(intentFilter.category)}
|
||||
</intent-filter>`;
|
||||
});
|
||||
}
|
||||
|
||||
function renderIntentFilterDatumEntries(datum: any) {
|
||||
return Object.entries(datum)
|
||||
.map((entry) => `android:${entry[0]}="${entry[1]}"`)
|
||||
.join(' ');
|
||||
}
|
||||
|
||||
function renderIntentFilterData(data: any) {
|
||||
return (Array.isArray(data) ? data : [data])
|
||||
.map((datum) => `<data ${renderIntentFilterDatumEntries(datum)}/>`)
|
||||
.join('\n');
|
||||
}
|
||||
|
||||
function renderIntentFilterCategory(category: any) {
|
||||
return (Array.isArray(category) ? category : [category])
|
||||
.map((cat) => `<category android:name="android.intent.category.${cat}"/>`)
|
||||
.join('\n');
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,76 @@
|
||||
import fs from 'fs-extra';
|
||||
import pMap from 'p-map';
|
||||
import pRetry from 'p-retry';
|
||||
import path from 'path';
|
||||
import logger from 'turtle/logger';
|
||||
import url from 'url';
|
||||
import urlJoin from 'url-join';
|
||||
|
||||
import { saveUrlToPathAsync } from './ExponentTools';
|
||||
import { AnyStandaloneContext } from './StandaloneContext';
|
||||
|
||||
const EXPO_DOMAINS = ['expo.io', 'exp.host', 'expo.test', 'localhost'];
|
||||
export const DEFAULT_CDN_HOST = 'https://d1wp6m56sqw74a.cloudfront.net';
|
||||
export const ASSETS_DIR_DEFAULT_URL = `${DEFAULT_CDN_HOST}/~assets`;
|
||||
|
||||
type UrlResolver = (hash: string) => string;
|
||||
|
||||
export async function bundleAsync(
|
||||
context: any,
|
||||
assets: string[],
|
||||
dest: string,
|
||||
exportUrl?: string | null,
|
||||
) {
|
||||
if (!assets) {
|
||||
return;
|
||||
}
|
||||
|
||||
await fs.ensureDir(dest);
|
||||
|
||||
const urlResolver = createAssetsUrlResolver(context, exportUrl);
|
||||
await pMap(assets, (asset) => downloadAssetAsync(urlResolver, dest, asset), {
|
||||
concurrency: 5,
|
||||
});
|
||||
}
|
||||
|
||||
async function downloadAssetAsync(
|
||||
urlResolver: UrlResolver,
|
||||
dest: string,
|
||||
asset: string,
|
||||
) {
|
||||
const extensionIndex = asset.lastIndexOf('.');
|
||||
const prefixLength = 'asset_'.length;
|
||||
const hash =
|
||||
extensionIndex >= 0
|
||||
? asset.substring(prefixLength, extensionIndex)
|
||||
: asset.substring(prefixLength);
|
||||
logger.info(urlResolver(hash));
|
||||
await pRetry(
|
||||
() => saveUrlToPathAsync(urlResolver(hash), path.join(dest, asset)),
|
||||
{ retries: 3 },
|
||||
);
|
||||
}
|
||||
|
||||
function createAssetsUrlResolver(
|
||||
context: AnyStandaloneContext,
|
||||
exportUrl?: string | null,
|
||||
): UrlResolver {
|
||||
let assetsDirUrl = exportUrl ? `${exportUrl}/assets` : ASSETS_DIR_DEFAULT_URL;
|
||||
|
||||
if (context && context.published && context.published.url) {
|
||||
const { assetUrlOverride = './assets' } = context.config;
|
||||
const publishedUrl = context.published.url;
|
||||
const { hostname } = url.parse(publishedUrl);
|
||||
if (hostname == null) {
|
||||
throw new Error(
|
||||
`Could not resolve asset URLs relative to "${publishedUrl}". Published URL must be an absolute URL.`,
|
||||
);
|
||||
}
|
||||
const maybeExpoDomain = hostname.split('.').slice(-2).join('.');
|
||||
if (!EXPO_DOMAINS.includes(maybeExpoDomain)) {
|
||||
assetsDirUrl = url.resolve(publishedUrl, assetUrlOverride);
|
||||
}
|
||||
}
|
||||
|
||||
return (hash) => urlJoin(assetsDirUrl, hash);
|
||||
}
|
||||
@@ -0,0 +1,565 @@
|
||||
// Set EXPO_VIEW_DIR to universe/exponent to test locally
|
||||
|
||||
import { findConfigFile, getConfig } from '@expo/config';
|
||||
import JsonFile from '@expo/json-file';
|
||||
import spawnAsync from '@expo/spawn-async';
|
||||
import fs from 'fs-extra';
|
||||
import { sync as globSync } from 'glob';
|
||||
import isPlainObject from 'lodash/isPlainObject';
|
||||
import path from 'path';
|
||||
import process from 'process';
|
||||
import prompts from 'prompts';
|
||||
import rimraf from 'rimraf';
|
||||
import uuid from 'uuid';
|
||||
|
||||
import { Versions, EmbeddedAssets, UrlUtils, UserManager, Api } from '@expo/xdl';
|
||||
|
||||
import XDLError from '../XDLError';
|
||||
|
||||
import * as AndroidShellApp from './AndroidShellApp';
|
||||
import * as AssetBundle from './AssetBundle';
|
||||
import { isDirectory, regexFileAsync, rimrafDontThrow } from './ExponentTools';
|
||||
import * as IosNSBundle from './IosNSBundle';
|
||||
import * as IosPlist from './IosPlist';
|
||||
import * as IosWorkspace from './IosWorkspace';
|
||||
import logger from './Logger';
|
||||
import StandaloneBuildFlags from './StandaloneBuildFlags';
|
||||
import StandaloneContext from './StandaloneContext';
|
||||
import installPackagesAsync from './installPackagesAsync';
|
||||
|
||||
const SERVICE_CONTEXT_PROJECT_NAME = 'exponent-view-template';
|
||||
|
||||
async function yesnoAsync(message) {
|
||||
const { ok } = await prompts({
|
||||
type: 'confirm',
|
||||
name: 'ok',
|
||||
initial: true,
|
||||
message,
|
||||
});
|
||||
return ok;
|
||||
}
|
||||
|
||||
export async function detachAsync(projectRoot, options = {}) {
|
||||
const originalLogger = logger.loggerObj;
|
||||
logger.configure({
|
||||
trace: options.verbose ? console.trace.bind(console) : () => {},
|
||||
debug: options.verbose ? console.debug.bind(console) : () => {},
|
||||
info: options.verbose ? console.info.bind(console) : () => {},
|
||||
warn: console.warn.bind(console),
|
||||
error: console.error.bind(console),
|
||||
fatal: console.error.bind(console),
|
||||
});
|
||||
try {
|
||||
return await _detachAsync(projectRoot, options);
|
||||
} finally {
|
||||
logger.configure(originalLogger);
|
||||
}
|
||||
}
|
||||
|
||||
async function _detachAsync(projectRoot, options) {
|
||||
const user = await UserManager.ensureLoggedInAsync();
|
||||
|
||||
if (!user) {
|
||||
throw new Error('Internal error -- somehow detach is being run in offline mode.');
|
||||
}
|
||||
|
||||
const username = user.username;
|
||||
const { configName, configPath, configNamespace } = findConfigFile(projectRoot);
|
||||
let { exp } = getConfig(projectRoot);
|
||||
const experienceName = `@${username}/${exp.slug}`;
|
||||
const experienceUrl = `exp://exp.host/${experienceName}`;
|
||||
|
||||
// Check to make sure project isn't fully detached already
|
||||
const hasIosDirectory = isDirectory(path.join(projectRoot, 'ios'));
|
||||
const hasAndroidDirectory = isDirectory(path.join(projectRoot, 'android'));
|
||||
|
||||
if (hasIosDirectory && hasAndroidDirectory) {
|
||||
throw new XDLError(
|
||||
'DIRECTORY_ALREADY_EXISTS',
|
||||
'Error detaching. `ios` and `android` directories already exist.'
|
||||
);
|
||||
}
|
||||
|
||||
// Project was already detached on Windows or Linux
|
||||
if (!hasIosDirectory && hasAndroidDirectory && process.platform === 'darwin') {
|
||||
const response = await yesnoAsync(
|
||||
`This will add an Xcode project and leave your existing Android project alone. Enter 'yes' to continue:`
|
||||
);
|
||||
if (!response) {
|
||||
logger.info('Exiting...');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (hasIosDirectory && !hasAndroidDirectory) {
|
||||
throw new Error('`ios` directory already exists. Please remove it and try again.');
|
||||
}
|
||||
|
||||
logger.info('Validating project manifest...');
|
||||
if (!exp.name) {
|
||||
throw new Error(`${configName} is missing \`name\``);
|
||||
}
|
||||
|
||||
if (!exp.sdkVersion) {
|
||||
throw new Error(`${configName} is missing \`sdkVersion\``);
|
||||
}
|
||||
|
||||
if (!Versions.gteSdkVersion(exp, '25.0.0')) {
|
||||
throw new Error(
|
||||
`The app must be updated to SDK 25.0.0 or newer to be compatible with this tool.`
|
||||
);
|
||||
}
|
||||
|
||||
const versions = await Versions.versionsAsync();
|
||||
let sdkVersionConfig = versions.sdkVersions[exp.sdkVersion];
|
||||
if (
|
||||
!sdkVersionConfig ||
|
||||
(!sdkVersionConfig.androidExpoViewUrl && !sdkVersionConfig.iosExpoViewUrl)
|
||||
) {
|
||||
if (process.env.EXPO_VIEW_DIR) {
|
||||
logger.warn(
|
||||
`Detaching is not supported for SDK ${exp.sdkVersion}; ignoring this because you provided EXPO_VIEW_DIR`
|
||||
);
|
||||
sdkVersionConfig = {};
|
||||
} else {
|
||||
throw new Error(`Detaching is not supported for SDK version ${exp.sdkVersion}`);
|
||||
}
|
||||
}
|
||||
|
||||
exp.isDetached = true;
|
||||
|
||||
if (!exp.detach) {
|
||||
exp.detach = {};
|
||||
}
|
||||
|
||||
const detachedUUID = uuid.v4().replace(/-/g, '');
|
||||
const generatedScheme = `exp${detachedUUID}`;
|
||||
|
||||
if (!exp.detach.scheme && !Versions.gteSdkVersion(exp, '27.0.0')) {
|
||||
// set this for legacy purposes
|
||||
exp.detach.scheme = generatedScheme;
|
||||
}
|
||||
|
||||
const linkingWarning = `You have not specified a custom scheme for deep linking. A default value of ${generatedScheme} will be used. You can change this later by following the instructions in this guide: https://docs.expo.io/workflow/linking/`;
|
||||
if (!exp.scheme) {
|
||||
logger.info(linkingWarning);
|
||||
exp.scheme = generatedScheme;
|
||||
} else if (Array.isArray(exp.scheme) && exp.scheme.length === 0) {
|
||||
logger.info(linkingWarning);
|
||||
exp.scheme.push(generatedScheme);
|
||||
}
|
||||
|
||||
const expoDirectory = path.join(projectRoot, '.expo-source');
|
||||
fs.mkdirpSync(expoDirectory);
|
||||
const context = StandaloneContext.createUserContext(projectRoot, exp, experienceUrl);
|
||||
|
||||
// iOS
|
||||
let isIosSupported = true;
|
||||
if (process.platform !== 'darwin') {
|
||||
if (options && options.force) {
|
||||
logger.warn(
|
||||
`You are not running macOS, but have provided the --force option, so we will attempt to generate an iOS project anyway. This might fail.`
|
||||
);
|
||||
} else {
|
||||
logger.warn(`Skipping iOS because you are not running macOS.`);
|
||||
isIosSupported = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasIosDirectory && isIosSupported && sdkVersionConfig.iosExpoViewUrl) {
|
||||
if (!exp.ios) {
|
||||
exp.ios = {};
|
||||
}
|
||||
if (!exp.ios.bundleIdentifier) {
|
||||
logger.info(
|
||||
`You'll need to specify an iOS bundle identifier. See: https://docs.expo.io/versions/latest/config/app/#ios`
|
||||
);
|
||||
const { iosBundleIdentifier } = await prompts({
|
||||
type: 'text',
|
||||
name: 'iosBundleIdentifier',
|
||||
message: 'What would you like your iOS bundle identifier to be?',
|
||||
validate: value => /^[a-zA-Z][a-zA-Z0-9\-.]+$/.test(value),
|
||||
});
|
||||
exp.ios.bundleIdentifier = iosBundleIdentifier;
|
||||
}
|
||||
|
||||
await detachIOSAsync(context);
|
||||
exp = IosWorkspace.addDetachedConfigToExp(exp, context);
|
||||
exp.detach.iosExpoViewUrl = sdkVersionConfig.iosExpoViewUrl;
|
||||
}
|
||||
|
||||
// Android
|
||||
if (!hasAndroidDirectory && sdkVersionConfig.androidExpoViewUrl) {
|
||||
if (!exp.android) {
|
||||
exp.android = {};
|
||||
}
|
||||
if (!exp.android.package) {
|
||||
logger.info(
|
||||
`You'll need to specify an Android package name. See: https://docs.expo.io/versions/latest/config/app/#android`
|
||||
);
|
||||
const { androidPackage } = await prompts({
|
||||
type: 'text',
|
||||
name: 'androidPackage',
|
||||
message: 'What would you like your Android package name to be?',
|
||||
validate: value =>
|
||||
/^[a-zA-Z][a-zA-Z0-9_]*(\.[a-zA-Z][a-zA-Z0-9_]*)+$/.test(value)
|
||||
? true
|
||||
: "Invalid format of Android package name (only alphanumeric characters, '.' and '_' are allowed, and each '.' must be followed by a letter)",
|
||||
});
|
||||
exp.android.package = androidPackage;
|
||||
}
|
||||
|
||||
const androidDirectory = path.join(expoDirectory, 'android');
|
||||
rimraf.sync(androidDirectory);
|
||||
fs.mkdirpSync(androidDirectory);
|
||||
await detachAndroidAsync(context, sdkVersionConfig.androidExpoViewUrl);
|
||||
exp = AndroidShellApp.addDetachedConfigToExp(exp, context);
|
||||
exp.detach.androidExpoViewUrl = sdkVersionConfig.androidExpoViewUrl;
|
||||
}
|
||||
|
||||
logger.info('Writing ExpoKit configuration...');
|
||||
// if we're writing to app.json, we need to place the configuration under the expo key
|
||||
const config = configNamespace ? { [configNamespace]: exp } : exp;
|
||||
await fs.writeFile(configPath, JSON.stringify(config, null, 2));
|
||||
|
||||
const packagesToInstall = [];
|
||||
const nodeModulesPath = exp.nodeModulesPath
|
||||
? path.resolve(projectRoot, exp.nodeModulesPath)
|
||||
: projectRoot;
|
||||
|
||||
if (sdkVersionConfig && sdkVersionConfig.expoReactNativeTag) {
|
||||
packagesToInstall.push(
|
||||
`react-native@https://github.com/expo/react-native/archive/${sdkVersionConfig.expoReactNativeTag}.tar.gz`
|
||||
);
|
||||
} else if (process.env.EXPO_VIEW_DIR) {
|
||||
// ignore, using test directory
|
||||
} else {
|
||||
throw new Error(`Expo's fork of react-native does not support this SDK version.`);
|
||||
}
|
||||
|
||||
// Add expokitNpmPackage if it is supported. Was added before SDK 29.
|
||||
if (process.env.EXPO_VIEW_DIR) {
|
||||
logger.info(`Linking 'expokit' package...`);
|
||||
await spawnAsync('yarn', ['link'], {
|
||||
cwd: path.join(process.env.EXPO_VIEW_DIR, 'expokit-npm-package'),
|
||||
});
|
||||
await spawnAsync('yarn', ['link', 'expokit'], {
|
||||
cwd: nodeModulesPath,
|
||||
});
|
||||
} else if (sdkVersionConfig.expokitNpmPackage) {
|
||||
packagesToInstall.push(sdkVersionConfig.expokitNpmPackage);
|
||||
}
|
||||
|
||||
if (sdkVersionConfig) {
|
||||
const { packagesToInstallWhenEjecting } = sdkVersionConfig;
|
||||
if (isPlainObject(packagesToInstallWhenEjecting)) {
|
||||
Object.keys(packagesToInstallWhenEjecting).forEach(packageName => {
|
||||
packagesToInstall.push(`${packageName}@${packagesToInstallWhenEjecting[packageName]}`);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (packagesToInstall.length) {
|
||||
await installPackagesAsync(projectRoot, packagesToInstall, {
|
||||
packageManager: options.packageManager,
|
||||
});
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a detached Expo iOS app pointing at the given project.
|
||||
*/
|
||||
async function detachIOSAsync(context) {
|
||||
await IosWorkspace.createDetachedAsync(context);
|
||||
|
||||
logger.info('Configuring iOS project...');
|
||||
await IosNSBundle.configureAsync(context);
|
||||
|
||||
logger.info(`iOS detach is complete!`);
|
||||
}
|
||||
|
||||
async function detachAndroidAsync(context, expoViewUrl) {
|
||||
if (context.type !== 'user') {
|
||||
throw new Error(`detachAndroidAsync only supports user standalone contexts`);
|
||||
}
|
||||
|
||||
logger.info('Moving Android project files...');
|
||||
const androidProjectDirectory = path.join(context.data.projectPath, 'android');
|
||||
let tmpExpoDirectory;
|
||||
if (process.env.EXPO_VIEW_DIR) {
|
||||
// Only for testing
|
||||
await AndroidShellApp.copyInitialShellAppFilesAsync(
|
||||
path.join(process.env.EXPO_VIEW_DIR, 'android'),
|
||||
androidProjectDirectory,
|
||||
true,
|
||||
context.data.exp.sdkVersion
|
||||
);
|
||||
} else {
|
||||
tmpExpoDirectory = path.join(context.data.projectPath, 'temp-android-directory');
|
||||
fs.mkdirpSync(tmpExpoDirectory);
|
||||
logger.info('Downloading Android code...');
|
||||
await Api.downloadAsync(expoViewUrl, tmpExpoDirectory, { extract: true });
|
||||
await AndroidShellApp.copyInitialShellAppFilesAsync(
|
||||
tmpExpoDirectory,
|
||||
androidProjectDirectory,
|
||||
true,
|
||||
context.data.exp.sdkVersion
|
||||
);
|
||||
}
|
||||
|
||||
logger.info('Updating Android app...');
|
||||
await AndroidShellApp.runShellAppModificationsAsync(context, context.data.exp.sdkVersion);
|
||||
|
||||
// Clean up
|
||||
logger.info('Cleaning up Android...');
|
||||
if (!process.env.EXPO_VIEW_DIR) {
|
||||
rimrafDontThrow(tmpExpoDirectory);
|
||||
}
|
||||
logger.info('Android detach is complete!\n');
|
||||
}
|
||||
|
||||
async function ensureBuildConstantsExistsIOSAsync(configFilePath) {
|
||||
// EXBuildConstants is included in newer ExpoKit projects.
|
||||
// create it if it doesn't exist.
|
||||
const doesBuildConstantsExist = fs.existsSync(
|
||||
path.join(configFilePath, 'EXBuildConstants.plist')
|
||||
);
|
||||
if (!doesBuildConstantsExist) {
|
||||
await IosPlist.createBlankAsync(configFilePath, 'EXBuildConstants');
|
||||
logger.info('Created `EXBuildConstants.plist` because it did not exist yet');
|
||||
}
|
||||
}
|
||||
|
||||
async function _getIosExpoKitVersionThrowErrorAsync(iosProjectDirectory) {
|
||||
let expoKitVersion = '';
|
||||
const podfileLockPath = path.join(iosProjectDirectory, 'Podfile.lock');
|
||||
try {
|
||||
const podfileLock = await fs.readFile(podfileLockPath, 'utf8');
|
||||
const expoKitVersionRegex = /ExpoKit\/Core\W?\(([0-9.]+)\)/gi;
|
||||
const match = expoKitVersionRegex.exec(podfileLock);
|
||||
expoKitVersion = match[1];
|
||||
} catch (e) {
|
||||
throw new Error(
|
||||
`Unable to read ExpoKit version from Podfile.lock. Make sure your project depends on ExpoKit. (${e})`
|
||||
);
|
||||
}
|
||||
return expoKitVersion;
|
||||
}
|
||||
|
||||
async function readNullableConfigJsonAsync(projectDir) {
|
||||
try {
|
||||
return getConfig(projectDir);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function prepareDetachedBuildIosAsync(projectDir, args) {
|
||||
const config = await readNullableConfigJsonAsync(projectDir);
|
||||
if (config && config.exp.name !== SERVICE_CONTEXT_PROJECT_NAME) {
|
||||
return prepareDetachedUserContextIosAsync(projectDir, config.exp, args);
|
||||
} else {
|
||||
return prepareDetachedServiceContextIosAsync(projectDir, args);
|
||||
}
|
||||
}
|
||||
|
||||
async function prepareDetachedServiceContextIosAsync(projectDir, args) {
|
||||
// service context
|
||||
// TODO: very brittle hack: the paths here are hard coded to match the single workspace
|
||||
// path generated inside IosShellApp. When we support more than one path, this needs to
|
||||
// be smarter.
|
||||
const expoRootDir = path.join(projectDir, '..', '..');
|
||||
const workspaceSourcePath = path.join(projectDir, 'ios');
|
||||
const buildFlags = StandaloneBuildFlags.createIos('Release', { workspaceSourcePath });
|
||||
const context = StandaloneContext.createServiceContext(
|
||||
expoRootDir,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
/* testEnvironment */ 'none',
|
||||
buildFlags,
|
||||
null,
|
||||
null
|
||||
);
|
||||
const { iosProjectDirectory, supportingDirectory } = IosWorkspace.getPaths(context);
|
||||
const expoKitVersion = await _getIosExpoKitVersionThrowErrorAsync(iosProjectDirectory);
|
||||
|
||||
// use prod api keys if available
|
||||
const prodApiKeys = await _readDefaultApiKeysAsync(
|
||||
path.join(context.data.expoSourcePath, '__internal__', 'keys.json')
|
||||
);
|
||||
|
||||
const { exp } = getConfig(expoRootDir, { skipSDKVersionRequirement: true });
|
||||
|
||||
await IosPlist.modifyAsync(supportingDirectory, 'EXBuildConstants', constantsConfig => {
|
||||
// verify that we are actually in a service context and not a misconfigured project
|
||||
const contextType = constantsConfig.STANDALONE_CONTEXT_TYPE;
|
||||
if (contextType !== 'service') {
|
||||
throw new Error(
|
||||
'Unable to configure a project which has no app.json and also no STANDALONE_CONTEXT_TYPE.'
|
||||
);
|
||||
}
|
||||
constantsConfig.EXPO_RUNTIME_VERSION = expoKitVersion;
|
||||
constantsConfig.API_SERVER_ENDPOINT =
|
||||
process.env.ENVIRONMENT === 'staging'
|
||||
? 'https://staging.exp.host/--/api/v2/'
|
||||
: 'https://exp.host/--/api/v2/';
|
||||
if (prodApiKeys) {
|
||||
constantsConfig.DEFAULT_API_KEYS = prodApiKeys;
|
||||
}
|
||||
if (exp && exp.sdkVersion) {
|
||||
constantsConfig.TEMPORARY_SDK_VERSION = exp.sdkVersion;
|
||||
}
|
||||
return constantsConfig;
|
||||
});
|
||||
}
|
||||
|
||||
async function _readDefaultApiKeysAsync(jsonFilePath) {
|
||||
if (fs.existsSync(jsonFilePath)) {
|
||||
const keys = {};
|
||||
const allKeys = await new JsonFile(jsonFilePath).readAsync();
|
||||
const validKeys = ['AMPLITUDE_KEY', 'GOOGLE_MAPS_IOS_API_KEY'];
|
||||
for (const key in allKeys) {
|
||||
if (allKeys.hasOwnProperty(key) && validKeys.includes(key)) {
|
||||
keys[key] = allKeys[key];
|
||||
}
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async function prepareDetachedUserContextIosAsync(projectDir, exp, args) {
|
||||
const context = StandaloneContext.createUserContext(projectDir, exp);
|
||||
const { iosProjectDirectory, supportingDirectory } = IosWorkspace.getPaths(context);
|
||||
|
||||
logger.info(`Preparing iOS build at ${iosProjectDirectory}...`);
|
||||
// These files cause @providesModule naming collisions
|
||||
// but are not available until after `pod install` has run.
|
||||
const podsDirectory = path.join(iosProjectDirectory, 'Pods');
|
||||
if (!isDirectory(podsDirectory)) {
|
||||
throw new Error(`Can't find directory ${podsDirectory}, make sure you've run pod install.`);
|
||||
}
|
||||
const rnPodDirectory = path.join(podsDirectory, 'React');
|
||||
if (isDirectory(rnPodDirectory)) {
|
||||
const rnFilesToDelete = globSync('**/*.@(js|json)', {
|
||||
absolute: true,
|
||||
cwd: rnPodDirectory,
|
||||
});
|
||||
if (rnFilesToDelete) {
|
||||
for (let i = 0; i < rnFilesToDelete.length; i++) {
|
||||
await fs.unlink(rnFilesToDelete[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// insert expo development url into iOS config
|
||||
if (!args.skipXcodeConfig) {
|
||||
// populate EXPO_RUNTIME_VERSION from ExpoKit pod version
|
||||
const expoKitVersion = await _getIosExpoKitVersionThrowErrorAsync(iosProjectDirectory);
|
||||
|
||||
// populate development url
|
||||
const devUrl = await UrlUtils.constructManifestUrlAsync(projectDir);
|
||||
|
||||
// populate default api keys
|
||||
const defaultApiKeys = await _readDefaultApiKeysAsync(
|
||||
path.join(podsDirectory, 'ExpoKit', 'template-files', 'keys.json')
|
||||
);
|
||||
|
||||
await ensureBuildConstantsExistsIOSAsync(supportingDirectory);
|
||||
await IosPlist.modifyAsync(supportingDirectory, 'EXBuildConstants', constantsConfig => {
|
||||
constantsConfig.developmentUrl = devUrl;
|
||||
constantsConfig.EXPO_RUNTIME_VERSION = expoKitVersion;
|
||||
if (defaultApiKeys) {
|
||||
constantsConfig.DEFAULT_API_KEYS = defaultApiKeys;
|
||||
}
|
||||
if (exp.sdkVersion) {
|
||||
constantsConfig.TEMPORARY_SDK_VERSION = exp.sdkVersion;
|
||||
}
|
||||
return constantsConfig;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function prepareDetachedBuildAsync(projectDir, args) {
|
||||
if (args.platform === 'ios') {
|
||||
await prepareDetachedBuildIosAsync(projectDir, args);
|
||||
} else {
|
||||
const expoBuildConstantsMatches = globSync('android/**/DetachBuildConstants.java', {
|
||||
absolute: true,
|
||||
cwd: projectDir,
|
||||
});
|
||||
if (expoBuildConstantsMatches && expoBuildConstantsMatches.length) {
|
||||
const expoBuildConstants = expoBuildConstantsMatches[0];
|
||||
const devUrl = await UrlUtils.constructManifestUrlAsync(projectDir);
|
||||
await regexFileAsync(
|
||||
/DEVELOPMENT_URL = "[^"]*";/,
|
||||
`DEVELOPMENT_URL = "${devUrl}";`,
|
||||
expoBuildConstants
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// args.dest: string,
|
||||
// This is the path where assets will be copied to. It should be
|
||||
// `$CONFIGURATION_BUILD_DIR/$UNLOCALIZED_RESOURCES_FOLDER_PATH` on iOS
|
||||
// (see `exponent-view-template.xcodeproj/project.pbxproj` for an example)
|
||||
// and `$buildDir/intermediates/assets/$targetPath` on Android (see
|
||||
// `android/app/expo.gradle` for an example).
|
||||
export async function bundleAssetsAsync(projectDir, args) {
|
||||
const options = await readNullableConfigJsonAsync(projectDir);
|
||||
if (!options || options.exp.name === SERVICE_CONTEXT_PROJECT_NAME) {
|
||||
// Don't run assets bundling for the service context.
|
||||
return;
|
||||
}
|
||||
const { exp } = options;
|
||||
const bundledManifestPath = EmbeddedAssets.getEmbeddedManifestPath(
|
||||
args.platform,
|
||||
projectDir,
|
||||
exp
|
||||
);
|
||||
if (!bundledManifestPath) {
|
||||
logger.warn(
|
||||
`Skipped assets bundling because the '${args.platform}.publishManifestPath' key is not specified in the app manifest.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
let manifest;
|
||||
try {
|
||||
manifest = JSON.parse(await fs.readFile(bundledManifestPath, 'utf8'));
|
||||
} catch (ex) {
|
||||
throw new Error(
|
||||
`Error reading the manifest file. Make sure the path '${bundledManifestPath}' is correct.\n\nError: ${ex.message}`
|
||||
);
|
||||
}
|
||||
if (!manifest || !Object.keys(manifest).length) {
|
||||
throw new Error(`The manifest at '${bundledManifestPath}' was empty or invalid.`);
|
||||
}
|
||||
|
||||
await AssetBundle.bundleAsync(null, manifest.bundledAssets, args.dest, getExportUrl(manifest));
|
||||
}
|
||||
|
||||
/**
|
||||
* This function extracts the exported public URL that is set in the manifest
|
||||
* when the developer runs `expo export --public-url x`. We use this to ensure
|
||||
* that we fetch the resources from the appropriate place when doing builds
|
||||
* against self-hosted apps.
|
||||
*/
|
||||
function getExportUrl(manifest) {
|
||||
const { bundleUrl } = manifest;
|
||||
if (bundleUrl.includes(AssetBundle.DEFAULT_CDN_HOST)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const bundleUrlParts = bundleUrl.split('/');
|
||||
return bundleUrlParts.slice(0, bundleUrlParts.length - 2).join('/');
|
||||
} catch (e) {
|
||||
throw Error(
|
||||
`Expected bundleUrl to be of the format https://domain/bundles/bundle-hash-id, ${bundleUrl} does not follow this format.`
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,297 @@
|
||||
import { ExpoConfig, Platform } from '@expo/config';
|
||||
import spawnAsyncQuiet, { SpawnOptions, SpawnResult } from '@expo/spawn-async';
|
||||
import axios from 'axios';
|
||||
import fs from 'fs-extra';
|
||||
import isObject from 'lodash/isObject';
|
||||
import path from 'path';
|
||||
import { Readable } from 'stream';
|
||||
|
||||
import XDLError from '../XDLError';
|
||||
import LoggerDetach, { Logger, pipeOutputToLogger } from './Logger';
|
||||
|
||||
function getManifestFileNameForSdkVersion(sdkVersion: string) {
|
||||
if (parseSdkMajorVersion(sdkVersion) < 39) {
|
||||
return 'shell-app-manifest.json';
|
||||
} else {
|
||||
return 'app.manifest';
|
||||
}
|
||||
}
|
||||
|
||||
function getBundleFileNameForSdkVersion(sdkVersion: string) {
|
||||
if (parseSdkMajorVersion(sdkVersion) < 39) {
|
||||
return 'shell-app.bundle';
|
||||
} else {
|
||||
return 'app.bundle';
|
||||
}
|
||||
}
|
||||
|
||||
function parseSdkMajorVersion(expSdkVersion: string) {
|
||||
// We assume that the unversioned SDK is the latest
|
||||
if (expSdkVersion === 'UNVERSIONED') {
|
||||
return Infinity;
|
||||
}
|
||||
|
||||
let sdkMajorVersion = 0;
|
||||
try {
|
||||
const versionComponents = expSdkVersion
|
||||
.split('.')
|
||||
.map((num) => parseInt(num, 10));
|
||||
sdkMajorVersion = versionComponents[0];
|
||||
} catch {
|
||||
sdkMajorVersion = 0;
|
||||
}
|
||||
return sdkMajorVersion;
|
||||
}
|
||||
|
||||
async function saveUrlToPathAsync(
|
||||
url: string,
|
||||
filePath: string,
|
||||
timeout = 20000,
|
||||
) {
|
||||
const response = await axios.get(url, { responseType: 'stream', timeout });
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const stream = fs.createWriteStream(filePath);
|
||||
stream.on('close', resolve);
|
||||
stream.on('error', reject);
|
||||
response.data.on('error', reject).pipe(stream);
|
||||
});
|
||||
}
|
||||
|
||||
async function saveImageToPathAsync(
|
||||
projectRoot: string,
|
||||
pathOrURL: string,
|
||||
outPath: string,
|
||||
) {
|
||||
const localPath = path.resolve(projectRoot, pathOrURL);
|
||||
|
||||
if (fs.existsSync(localPath)) {
|
||||
await fs.copy(localPath, outPath);
|
||||
} else {
|
||||
await saveUrlToPathAsync(pathOrURL, outPath, 0);
|
||||
}
|
||||
}
|
||||
|
||||
async function getManifestAsync(url: string, headers: any, options: any = {}) {
|
||||
const buildPhaseLogger =
|
||||
options.logger ||
|
||||
LoggerDetach.withFields({ buildPhase: 'reading manifest' });
|
||||
|
||||
let response;
|
||||
try {
|
||||
response = await _retryPromise(() =>
|
||||
axios.get(url.replace('exp://', 'http://'), { headers }),
|
||||
);
|
||||
} catch (err) {
|
||||
buildPhaseLogger.error(err);
|
||||
throw new Error('Failed to fetch manifest from www');
|
||||
}
|
||||
|
||||
buildPhaseLogger.info(
|
||||
'Using manifest:',
|
||||
JSON.stringify(response.data, null, 2),
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async function _retryPromise<T>(
|
||||
fn: (...args: any[]) => T,
|
||||
retries = 5,
|
||||
): Promise<T> {
|
||||
try {
|
||||
return await fn();
|
||||
} catch (err) {
|
||||
if (retries-- > 0) {
|
||||
return await _retryPromise(fn, retries);
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export type AsyncSpawnOptions = SpawnOptions & {
|
||||
loggerFields?: any;
|
||||
pipeToLogger?: boolean | { stdout?: boolean; stderr?: boolean };
|
||||
stdoutOnly?: boolean;
|
||||
loggerLineTransformer?: (line: any) => any;
|
||||
};
|
||||
|
||||
async function spawnAsyncThrowError(
|
||||
command: string,
|
||||
args: string[],
|
||||
options: AsyncSpawnOptions = {
|
||||
stdio: 'inherit',
|
||||
cwd: process.cwd(),
|
||||
},
|
||||
): Promise<SpawnResult> {
|
||||
const { pipeToLogger } = options;
|
||||
if (pipeToLogger) {
|
||||
options.stdio = 'pipe';
|
||||
options.cwd = options.cwd || process.cwd();
|
||||
}
|
||||
const promise = spawnAsyncQuiet(command, args, options);
|
||||
if (pipeToLogger && promise.child) {
|
||||
const streams: { stdout?: Readable | null; stderr?: Readable | null } = {};
|
||||
if (pipeToLogger === true || pipeToLogger.stdout) {
|
||||
streams.stdout = promise.child.stdout;
|
||||
}
|
||||
if (pipeToLogger === true || pipeToLogger.stderr) {
|
||||
streams.stderr = promise.child.stderr;
|
||||
}
|
||||
pipeOutputToLogger(streams, options.loggerFields, options);
|
||||
}
|
||||
return promise;
|
||||
}
|
||||
|
||||
async function spawnAsync(
|
||||
command: string,
|
||||
args: string[],
|
||||
options: SpawnOptions,
|
||||
): Promise<SpawnResult | void> {
|
||||
try {
|
||||
return await spawnAsyncThrowError(command, args, options);
|
||||
} catch (e) {
|
||||
LoggerDetach.error(e.message);
|
||||
}
|
||||
}
|
||||
|
||||
function createSpawner(buildPhase: string, logger?: Logger) {
|
||||
return (command: string, ...args: any[]) => {
|
||||
const lastArg = args[args.length - 1];
|
||||
const optionsFromArg = isObject(lastArg) ? args.pop() : {};
|
||||
|
||||
const options = { ...optionsFromArg, pipeToLogger: true };
|
||||
if (buildPhase) {
|
||||
options.loggerFields = options.loggerFields ? options.loggerFields : {};
|
||||
options.loggerFields = { ...options.loggerFields, buildPhase };
|
||||
}
|
||||
|
||||
if (logger) {
|
||||
logger.info('Executing command:', command, ...args);
|
||||
}
|
||||
return spawnAsyncThrowError(command, args, options);
|
||||
};
|
||||
}
|
||||
|
||||
async function transformFileContentsAsync(
|
||||
filename: string,
|
||||
transform: (input: string) => string | null,
|
||||
) {
|
||||
const fileString = await fs.readFile(filename, 'utf8');
|
||||
const newFileString = transform(fileString);
|
||||
if (newFileString !== null) {
|
||||
await fs.writeFile(filename, newFileString);
|
||||
}
|
||||
}
|
||||
|
||||
function manifestUsesSplashApi(manifest: ExpoConfig, platform: Platform) {
|
||||
if (platform === 'ios') {
|
||||
return manifest.splash || (manifest.ios && manifest.ios.splash);
|
||||
}
|
||||
if (platform === 'android') {
|
||||
return manifest.splash || (manifest.android && manifest.android.splash);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function rimrafDontThrow(directory: string) {
|
||||
fs.removeSync(directory);
|
||||
}
|
||||
|
||||
async function removeIfExists(file: string) {
|
||||
await fs.remove(file);
|
||||
}
|
||||
|
||||
function isDirectory(dir: string) {
|
||||
try {
|
||||
if (fs.statSync(dir).isDirectory()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
interface ILocaleMap {
|
||||
[lang: string]: any;
|
||||
}
|
||||
|
||||
async function getResolvedLocalesAsync(
|
||||
projectRoot: string,
|
||||
exp: ExpoConfig,
|
||||
): Promise<ILocaleMap> {
|
||||
const locales: ILocaleMap = {};
|
||||
if (exp.locales !== undefined) {
|
||||
for (const [lang, localePath] of Object.entries(exp.locales)) {
|
||||
const s = await fs.readFile(
|
||||
path.resolve(projectRoot, localePath as string),
|
||||
'utf8',
|
||||
);
|
||||
try {
|
||||
locales[lang] = JSON.parse(s);
|
||||
} catch (e) {
|
||||
throw new XDLError('INVALID_JSON', JSON.stringify(e));
|
||||
}
|
||||
}
|
||||
}
|
||||
return locales;
|
||||
}
|
||||
|
||||
async function regexFileAsync(
|
||||
regex: RegExp | string,
|
||||
replace: string,
|
||||
filename: string,
|
||||
): Promise<void> {
|
||||
const file = await fs.readFile(filename);
|
||||
const fileString = file.toString();
|
||||
await fs.writeFile(filename, fileString.replace(regex, replace));
|
||||
}
|
||||
|
||||
// Matches sed /d behavior
|
||||
async function deleteLinesInFileAsync(
|
||||
startRegex: RegExp | string,
|
||||
endRegex: RegExp | string,
|
||||
filename: string,
|
||||
): Promise<void> {
|
||||
const file = await fs.readFile(filename);
|
||||
const fileString = file.toString();
|
||||
const lines = fileString.split(/\r?\n/);
|
||||
const filteredLines = [];
|
||||
let inDeleteRange = false;
|
||||
for (const line of lines) {
|
||||
if (line.match(startRegex)) {
|
||||
inDeleteRange = true;
|
||||
}
|
||||
|
||||
if (!inDeleteRange) {
|
||||
filteredLines.push(line);
|
||||
}
|
||||
|
||||
if (inDeleteRange && line.match(endRegex)) {
|
||||
inDeleteRange = false;
|
||||
}
|
||||
}
|
||||
await fs.writeFile(filename, filteredLines.join('\n'));
|
||||
}
|
||||
|
||||
export {
|
||||
isDirectory,
|
||||
parseSdkMajorVersion,
|
||||
saveUrlToPathAsync,
|
||||
saveImageToPathAsync,
|
||||
getManifestAsync,
|
||||
rimrafDontThrow,
|
||||
removeIfExists,
|
||||
spawnAsyncThrowError,
|
||||
spawnAsync,
|
||||
transformFileContentsAsync,
|
||||
manifestUsesSplashApi,
|
||||
getResolvedLocalesAsync,
|
||||
regexFileAsync,
|
||||
deleteLinesInFileAsync,
|
||||
createSpawner,
|
||||
getManifestFileNameForSdkVersion,
|
||||
getBundleFileNameForSdkVersion,
|
||||
};
|
||||
@@ -0,0 +1,71 @@
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
|
||||
import { parseSdkMajorVersion, spawnAsyncThrowError } from './ExponentTools';
|
||||
import * as IosIcons from './IosIcons';
|
||||
import { AnyStandaloneContext, StandaloneContextService } from './StandaloneContext';
|
||||
|
||||
/**
|
||||
* Compile a .car file from the icons in a manifest.
|
||||
*/
|
||||
async function buildAssetArchiveAsync(
|
||||
context: AnyStandaloneContext,
|
||||
destinationCARPath: string,
|
||||
intermediatesDirectory: string,
|
||||
) {
|
||||
if (!(context instanceof StandaloneContextService)) {
|
||||
throw new Error('buildAssetArchive is only supported for service standalone contexts.');
|
||||
}
|
||||
const { data } = context;
|
||||
fs.mkdirpSync(intermediatesDirectory);
|
||||
|
||||
// copy expoSourceRoot/.../Images.xcassets into intermediates
|
||||
await spawnAsyncThrowError(
|
||||
'/bin/cp',
|
||||
[
|
||||
'-R',
|
||||
path.join(data.expoSourcePath, 'Exponent', 'Images.xcassets'),
|
||||
path.join(intermediatesDirectory, 'Images.xcassets'),
|
||||
],
|
||||
{
|
||||
stdio: 'inherit',
|
||||
},
|
||||
);
|
||||
|
||||
// make the new xcassets contain the project's icon
|
||||
await IosIcons.createAndWriteIconsToPathAsync(
|
||||
context,
|
||||
path.join(intermediatesDirectory, 'Images.xcassets', 'AppIcon.appiconset'),
|
||||
);
|
||||
|
||||
const sdkMajorVersion = parseSdkMajorVersion(data.manifest.sdkVersion);
|
||||
const deploymentTarget = sdkMajorVersion > 40 ? '11.0' : '10.0'; // SDK41 drops support for iOS 10.0
|
||||
|
||||
// compile asset archive
|
||||
// prettier-ignore
|
||||
const xcrunargs = [
|
||||
'actool',
|
||||
'--minimum-deployment-target', deploymentTarget,
|
||||
'--platform', 'iphoneos',
|
||||
'--app-icon', 'AppIcon',
|
||||
'--output-partial-info-plist', 'assetcatalog_generated_info.plist',
|
||||
'--compress-pngs',
|
||||
'--enable-on-demand-resources', 'YES',
|
||||
'--product-type', 'com.apple.product-type.application',
|
||||
'--target-device', 'iphone',
|
||||
'--target-device', 'ipad',
|
||||
'--compile', path.relative(intermediatesDirectory, destinationCARPath),
|
||||
'Images.xcassets',
|
||||
];
|
||||
/*
|
||||
* Note: if you want to debug issues with `actool`, try changing to stdio: 'inherit'.
|
||||
* In both success and failure cases, actool will write an enormous .plist to stdout
|
||||
* which may contain the key `com.apple.actool.errors`. Great work Apple
|
||||
*/
|
||||
await spawnAsyncThrowError('xcrun', xcrunargs, {
|
||||
stdio: ['ignore', 'ignore', 'inherit'], // only stderr
|
||||
cwd: intermediatesDirectory,
|
||||
});
|
||||
}
|
||||
|
||||
export { buildAssetArchiveAsync };
|
||||
@@ -0,0 +1,343 @@
|
||||
import plist from '@expo/plist';
|
||||
import crypto from 'crypto';
|
||||
import fs from 'fs-extra';
|
||||
import { sync as globSync } from 'glob';
|
||||
import omit from 'lodash/omit';
|
||||
import minimatch from 'minimatch';
|
||||
import path from 'path';
|
||||
|
||||
import { spawnAsyncThrowError } from './ExponentTools';
|
||||
import { findP12CertSerialNumber, getP12CertFingerprint } from './PKCS12Utils';
|
||||
|
||||
async function ensureCertificateValid({ certPath, certPassword, teamID }) {
|
||||
const certData = await fs.readFile(certPath);
|
||||
const fingerprint = getP12CertFingerprint(certData, certPassword);
|
||||
const identities = await _findIdentitiesByTeamID(teamID);
|
||||
const isValid = identities.indexOf(fingerprint) !== -1;
|
||||
if (!isValid) {
|
||||
throw new Error(`codesign ident not present in find-identity: ${fingerprint}\n${identities}`);
|
||||
}
|
||||
return fingerprint;
|
||||
}
|
||||
|
||||
async function _findIdentitiesByTeamID(teamID) {
|
||||
const { output } = await spawnAsyncThrowError(
|
||||
'security',
|
||||
['find-identity', '-v', '-s', `(${teamID})`],
|
||||
{
|
||||
stdio: 'pipe',
|
||||
}
|
||||
);
|
||||
return output.join('');
|
||||
}
|
||||
|
||||
function validateProvisioningProfile(plistData, { distCertFingerprint, bundleIdentifier }) {
|
||||
_ensureDeveloperCertificateIsValid(plistData, distCertFingerprint);
|
||||
_ensureBundleIdentifierIsValid(plistData, bundleIdentifier);
|
||||
}
|
||||
|
||||
function _ensureDeveloperCertificateIsValid(plistData, distCertFingerprint) {
|
||||
const devCertBase64 = plistData.DeveloperCertificates[0];
|
||||
const devCertFingerprint = _genDerCertFingerprint(devCertBase64);
|
||||
if (devCertFingerprint !== distCertFingerprint) {
|
||||
throw new Error(
|
||||
'validateProvisioningProfile: provisioning profile is not associated with uploaded distribution certificate'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function _genDerCertFingerprint(certBase64) {
|
||||
const certBuffer = Buffer.from(certBase64, 'base64');
|
||||
return crypto.createHash('sha1').update(certBuffer).digest('hex').toUpperCase();
|
||||
}
|
||||
|
||||
function _ensureBundleIdentifierIsValid(plistData, expectedBundleIdentifier) {
|
||||
const actualApplicationIdentifier = plistData.Entitlements['application-identifier'];
|
||||
const actualBundleIdentifier = /\.(.+)/.exec(actualApplicationIdentifier)[1];
|
||||
|
||||
if (!minimatch(expectedBundleIdentifier, actualBundleIdentifier)) {
|
||||
throw new Error(
|
||||
`validateProvisioningProfile: wrong bundleIdentifier found in provisioning profile; expected: ${expectedBundleIdentifier}, found (in provisioning profile): ${actualBundleIdentifier}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function writeExportOptionsPlistFile(plistPath, data) {
|
||||
const toWrite = createExportOptionsPlist(data);
|
||||
await fs.writeFile(plistPath, toWrite);
|
||||
}
|
||||
|
||||
const createExportOptionsPlist = ({
|
||||
bundleIdentifier,
|
||||
provisioningProfileUUID,
|
||||
exportMethod,
|
||||
teamID,
|
||||
}) => {
|
||||
const disableBitcodeCompiling = `<key>uploadBitcode</key>
|
||||
<false/>
|
||||
<key>compileBitcode</key>
|
||||
<false/>
|
||||
<key>uploadSymbols</key>
|
||||
<false/>`;
|
||||
return `<?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>method</key>
|
||||
<string>${exportMethod}</string>
|
||||
<key>teamID</key>
|
||||
<string>${teamID}</string>
|
||||
<key>provisioningProfiles</key>
|
||||
<dict>
|
||||
<key>${bundleIdentifier}</key>
|
||||
<string>${provisioningProfileUUID}</string>
|
||||
</dict>
|
||||
${exportMethod === 'ad-hoc' || exportMethod === 'enterprise' ? disableBitcodeCompiling : ''}
|
||||
</dict>
|
||||
</plist>`;
|
||||
};
|
||||
|
||||
async function buildIPA(
|
||||
{
|
||||
ipaPath,
|
||||
workspacePath,
|
||||
archivePath,
|
||||
codeSignIdentity,
|
||||
exportOptionsPlistPath,
|
||||
plistData,
|
||||
keychainPath,
|
||||
exportMethod,
|
||||
},
|
||||
credentials,
|
||||
client = false
|
||||
) {
|
||||
if (client) {
|
||||
await spawnAsyncThrowError(
|
||||
'xcodebuild',
|
||||
[
|
||||
'-exportArchive',
|
||||
'-archivePath',
|
||||
archivePath,
|
||||
'-exportOptionsPlist',
|
||||
exportOptionsPlistPath,
|
||||
'-exportPath',
|
||||
path.Dir(ipaPath),
|
||||
`OTHER_CODE_SIGN_FLAGS="--keychain ${keychainPath}"`,
|
||||
],
|
||||
{
|
||||
env: { ...process.env, CI: 1 },
|
||||
}
|
||||
);
|
||||
} else {
|
||||
await runFastlane(
|
||||
credentials,
|
||||
[
|
||||
'gym',
|
||||
'-n',
|
||||
path.basename(ipaPath),
|
||||
'--workspace',
|
||||
workspacePath,
|
||||
'--archive_path',
|
||||
archivePath,
|
||||
'--skip_build_archive',
|
||||
'true',
|
||||
'-i',
|
||||
codeSignIdentity,
|
||||
'--export_options',
|
||||
exportOptionsPlistPath,
|
||||
'--export_method',
|
||||
exportMethod,
|
||||
'--export_xcargs',
|
||||
`OTHER_CODE_SIGN_FLAGS="--keychain ${keychainPath}"`,
|
||||
'-o',
|
||||
path.dirname(ipaPath),
|
||||
'--verbose',
|
||||
],
|
||||
{ buildPhase: 'building and signing IPA' }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const resolveExportMethod = plistData => {
|
||||
if (plistData.ProvisionedDevices) {
|
||||
return 'ad-hoc';
|
||||
} else if (plistData.ProvisionsAllDevices === true) {
|
||||
return 'enterprise';
|
||||
} else {
|
||||
return 'app-store';
|
||||
}
|
||||
};
|
||||
|
||||
const entitlementTransferRules = [
|
||||
'com.apple.developer.associated-domains',
|
||||
'com.apple.developer.healthkit',
|
||||
'com.apple.developer.homekit',
|
||||
'com.apple.developer.icloud-container-identifiers',
|
||||
'com.apple.developer.icloud-services',
|
||||
'com.apple.developer.in-app-payments',
|
||||
'com.apple.developer.networking.vpn.api',
|
||||
'com.apple.developer.ubiquity-container-identifiers',
|
||||
'com.apple.developer.ubiquity-kvstore-identifier',
|
||||
'com.apple.external-accessory.wireless-configuration',
|
||||
'com.apple.security.application-groups',
|
||||
'inter-app-audio',
|
||||
'keychain-access-groups',
|
||||
];
|
||||
|
||||
const blacklistedEntitlementKeysWithoutICloud = [
|
||||
'com.apple.developer.icloud-container-environment',
|
||||
'com.apple.developer.icloud-container-identifiers',
|
||||
'com.apple.developer.icloud-services',
|
||||
'com.apple.developer.ubiquity-container-identifiers',
|
||||
'com.apple.developer.ubiquity-kvstore-identifier',
|
||||
];
|
||||
|
||||
const blacklistedEntitlementKeys = [
|
||||
'com.apple.developer.icloud-container-development-container-identifiers',
|
||||
'com.apple.developer.restricted-resource-mode',
|
||||
'inter-app-audio',
|
||||
'com.apple.developer.homekit',
|
||||
'com.apple.developer.healthkit',
|
||||
'com.apple.developer.in-app-payments',
|
||||
'com.apple.developer.maps',
|
||||
'com.apple.external-accessory.wireless-configuration',
|
||||
];
|
||||
|
||||
const icloudContainerEnvKey = 'com.apple.developer.icloud-container-environment';
|
||||
|
||||
async function createEntitlementsFile({
|
||||
generatedEntitlementsPath,
|
||||
plistData,
|
||||
archivePath,
|
||||
manifest,
|
||||
}) {
|
||||
const decodedProvisioningProfileEntitlements = plistData.Entitlements;
|
||||
|
||||
const entitlementsPaths = globSync('Products/Applications/*.app/*.entitlements', {
|
||||
absolute: true,
|
||||
cwd: archivePath,
|
||||
});
|
||||
if (entitlementsPaths.length === 0) {
|
||||
throw new Error("Didn't find any generated entitlements file in archive.");
|
||||
} else if (entitlementsPaths.length !== 1) {
|
||||
throw new Error('Found more than one entitlements file.');
|
||||
}
|
||||
const archiveEntitlementsPath = entitlementsPaths[0];
|
||||
const archiveEntitlementsRaw = await fs.readFile(archiveEntitlementsPath, 'utf8');
|
||||
let archiveEntitlementsData;
|
||||
try {
|
||||
archiveEntitlementsData = plist.parse(archiveEntitlementsRaw);
|
||||
} catch (error) {
|
||||
throw new Error(`Error when parsing plist: ${error.message}`);
|
||||
}
|
||||
|
||||
const entitlements = { ...decodedProvisioningProfileEntitlements };
|
||||
|
||||
entitlementTransferRules.forEach(rule => {
|
||||
if (rule in archiveEntitlementsData) {
|
||||
entitlements[rule] = archiveEntitlementsData[rule];
|
||||
}
|
||||
});
|
||||
|
||||
let generatedEntitlements = omit(entitlements, blacklistedEntitlementKeys);
|
||||
|
||||
if (!manifest.ios.usesIcloudStorage) {
|
||||
generatedEntitlements = omit(generatedEntitlements, blacklistedEntitlementKeysWithoutICloud);
|
||||
} else {
|
||||
const ubiquityKvKey = 'com.apple.developer.ubiquity-kvstore-identifier';
|
||||
if (generatedEntitlements[ubiquityKvKey]) {
|
||||
const teamId = generatedEntitlements[ubiquityKvKey].split('.')[0];
|
||||
generatedEntitlements[ubiquityKvKey] = `${teamId}.${manifest.ios.bundleIdentifier}`;
|
||||
}
|
||||
generatedEntitlements['com.apple.developer.icloud-services'] = ['CloudDocuments'];
|
||||
}
|
||||
if (!manifest.ios.associatedDomains) {
|
||||
generatedEntitlements = omit(generatedEntitlements, 'com.apple.developer.associated-domains');
|
||||
}
|
||||
if (!manifest.ios.usesAppleSignIn) {
|
||||
generatedEntitlements = omit(generatedEntitlements, 'com.apple.developer.applesignin');
|
||||
}
|
||||
if (generatedEntitlements[icloudContainerEnvKey]) {
|
||||
const envs = generatedEntitlements[icloudContainerEnvKey].filter(i => i === 'Production');
|
||||
generatedEntitlements[icloudContainerEnvKey] = envs;
|
||||
}
|
||||
|
||||
const generatedEntitlementsPlistData = plist.build(generatedEntitlements);
|
||||
await fs.writeFile(generatedEntitlementsPath, generatedEntitlementsPlistData, {
|
||||
mode: 0o755,
|
||||
});
|
||||
const { output } = await spawnAsyncThrowError(
|
||||
'/usr/libexec/PlistBuddy',
|
||||
['-x', '-c', 'Print', generatedEntitlementsPath],
|
||||
{
|
||||
stdio: 'pipe',
|
||||
}
|
||||
);
|
||||
const plistDataReformatted = output.join('');
|
||||
await fs.writeFile(generatedEntitlementsPath, plistDataReformatted, {
|
||||
mode: 0o755,
|
||||
});
|
||||
}
|
||||
|
||||
async function resignIPA(
|
||||
{
|
||||
codeSignIdentity,
|
||||
entitlementsPath,
|
||||
provisioningProfilePath,
|
||||
sourceIpaPath,
|
||||
destIpaPath,
|
||||
keychainPath,
|
||||
},
|
||||
credentials
|
||||
) {
|
||||
await spawnAsyncThrowError('cp', ['-rf', sourceIpaPath, destIpaPath]);
|
||||
await runFastlane(
|
||||
credentials,
|
||||
[
|
||||
'sigh',
|
||||
'resign',
|
||||
'--verbose',
|
||||
'--entitlements',
|
||||
entitlementsPath,
|
||||
'--signing_identity',
|
||||
codeSignIdentity,
|
||||
'--keychain_path',
|
||||
keychainPath,
|
||||
'--provisioning_profile',
|
||||
provisioningProfilePath,
|
||||
destIpaPath,
|
||||
],
|
||||
{ buildPhase: 'building and signing IPA' }
|
||||
);
|
||||
}
|
||||
|
||||
async function runFastlane({ teamID }, fastlaneArgs, loggerFields) {
|
||||
const fastlaneEnvVars = {
|
||||
FASTLANE_SKIP_UPDATE_CHECK: 1,
|
||||
FASTLANE_DISABLE_COLORS: 1,
|
||||
FASTLANE_TEAM_ID: teamID,
|
||||
CI: 1,
|
||||
LC_ALL: 'en_US.UTF-8',
|
||||
};
|
||||
|
||||
await spawnAsyncThrowError('fastlane', fastlaneArgs, {
|
||||
env: {
|
||||
...process.env,
|
||||
...fastlaneEnvVars,
|
||||
},
|
||||
pipeToLogger: true,
|
||||
dontShowStdout: false,
|
||||
loggerFields,
|
||||
});
|
||||
}
|
||||
|
||||
export {
|
||||
ensureCertificateValid,
|
||||
findP12CertSerialNumber,
|
||||
validateProvisioningProfile,
|
||||
writeExportOptionsPlistFile,
|
||||
buildIPA,
|
||||
resolveExportMethod,
|
||||
createEntitlementsFile,
|
||||
resignIPA,
|
||||
};
|
||||
@@ -0,0 +1,140 @@
|
||||
import plist from '@expo/plist';
|
||||
import fs from 'fs-extra';
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
|
||||
import { spawnAsyncThrowError } from './ExponentTools';
|
||||
import * as IosCodeSigning from './IosCodeSigning';
|
||||
import _logger from './Logger';
|
||||
|
||||
const logger = _logger.withFields({ buildPhase: 'building and signing IPA' });
|
||||
|
||||
export default function createIPABuilder(buildParams) {
|
||||
const { appUUID, keychainPath, bundleIdentifier, teamID, manifest, workspacePath } = buildParams;
|
||||
const appDir = path.join('/private/tmp/turtle', appUUID);
|
||||
const buildDir = path.join(appDir, 'build');
|
||||
const provisionDir = path.join(appDir, 'provisioning');
|
||||
const outputPath = path.join(appDir, 'archive.xcarchive');
|
||||
const uploadPath = path.join(buildDir, 'archive.ipa');
|
||||
|
||||
async function build() {
|
||||
const { provisioningProfilePath, clientBuild } = buildParams;
|
||||
|
||||
await copyProvisioningProfileToHomedir(provisioningProfilePath, appUUID);
|
||||
logger.info('provisioning profile copied to home directory');
|
||||
|
||||
try {
|
||||
const plistData = await readCMSMessage(provisioningProfilePath);
|
||||
logger.info('done retrieving provisioning profile data');
|
||||
|
||||
logger.info('checking if teamID is present in keychain and that certificate is valid...');
|
||||
const codeSignIdentity = await IosCodeSigning.ensureCertificateValid(buildParams);
|
||||
logger.info('ensured certificate is valid');
|
||||
|
||||
logger.info('validating provisioning profile...');
|
||||
IosCodeSigning.validateProvisioningProfile(plistData, {
|
||||
distCertFingerprint: codeSignIdentity,
|
||||
teamID,
|
||||
bundleIdentifier,
|
||||
});
|
||||
logger.info('provisioning profile is valid');
|
||||
|
||||
logger.info('writing export-options.plist file...');
|
||||
const exportMethod = IosCodeSigning.resolveExportMethod(plistData);
|
||||
const exportOptionsPlistPath = path.join(provisionDir, 'export-options.plist');
|
||||
const exportOptionsData = {
|
||||
bundleIdentifier,
|
||||
provisioningProfileUUID: plistData.UUID,
|
||||
exportMethod,
|
||||
teamID,
|
||||
};
|
||||
await IosCodeSigning.writeExportOptionsPlistFile(exportOptionsPlistPath, exportOptionsData);
|
||||
logger.info('created export-options.plist file');
|
||||
|
||||
logger.info('generating IPA...');
|
||||
const unsignedIpaPath = path.join(buildDir, `${appUUID}-unsigned.ipa`);
|
||||
const ipaBuilderArgs = {
|
||||
ipaPath: unsignedIpaPath,
|
||||
workspacePath,
|
||||
archivePath: outputPath,
|
||||
codeSignIdentity,
|
||||
exportOptionsPlistPath,
|
||||
plistData,
|
||||
keychainPath,
|
||||
exportMethod,
|
||||
};
|
||||
await IosCodeSigning.buildIPA(ipaBuilderArgs, buildParams, clientBuild);
|
||||
logger.info('generated IPA');
|
||||
|
||||
logger.info('creating entitlements file...');
|
||||
const generatedEntitlementsPath = path.join(appDir, 'generatedEntitlements.entitlements');
|
||||
await IosCodeSigning.createEntitlementsFile({
|
||||
generatedEntitlementsPath,
|
||||
plistData,
|
||||
archivePath: outputPath,
|
||||
manifest,
|
||||
});
|
||||
logger.info('created entitlements file');
|
||||
|
||||
logger.info('resigning IPA...');
|
||||
await IosCodeSigning.resignIPA(
|
||||
{
|
||||
codeSignIdentity,
|
||||
entitlementsPath: generatedEntitlementsPath,
|
||||
provisioningProfilePath,
|
||||
sourceIpaPath: unsignedIpaPath,
|
||||
destIpaPath: uploadPath,
|
||||
keychainPath,
|
||||
},
|
||||
buildParams
|
||||
);
|
||||
logger.info('resigned IPA');
|
||||
} finally {
|
||||
await removeProvisioningProfileFromHomedir(appUUID);
|
||||
logger.info('removed provisioning profile from the home directory');
|
||||
}
|
||||
}
|
||||
|
||||
async function cleanup() {
|
||||
try {
|
||||
await fs.remove(getProvisioningProfilePath(appUUID));
|
||||
} catch (err) {
|
||||
logger.error('failed to perform cleanup, error:', err);
|
||||
}
|
||||
}
|
||||
|
||||
async function copyProvisioningProfileToHomedir(provisioningProfilePath, appUUID) {
|
||||
await fs.mkdirp(getProvisioningProfileDirPath());
|
||||
const newProvisioningProfilePath = getProvisioningProfilePath(appUUID);
|
||||
await fs.copy(provisioningProfilePath, newProvisioningProfilePath);
|
||||
}
|
||||
|
||||
async function removeProvisioningProfileFromHomedir(appUUID) {
|
||||
const provisioningProfilePath = getProvisioningProfilePath(appUUID);
|
||||
await fs.remove(provisioningProfilePath);
|
||||
}
|
||||
|
||||
async function readCMSMessage(provisioningProfilePath) {
|
||||
const { output } = await spawnAsyncThrowError(
|
||||
'security',
|
||||
['cms', '-D', '-i', provisioningProfilePath],
|
||||
{
|
||||
stdio: 'pipe',
|
||||
}
|
||||
);
|
||||
const plistRaw = output.join('');
|
||||
try {
|
||||
return plist.parse(plistRaw);
|
||||
} catch (error) {
|
||||
throw new Error(`Error when parsing plist: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
const getProvisioningProfileDirPath = () =>
|
||||
path.join(os.homedir(), 'Library/MobileDevice/Provisioning Profiles');
|
||||
|
||||
const getProvisioningProfilePath = appUUID =>
|
||||
path.join(getProvisioningProfileDirPath(), `${appUUID}.mobileprovision`);
|
||||
|
||||
return { build, cleanup };
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
import path from 'path';
|
||||
|
||||
import { getImageDimensionsAsync, resizeImageAsync } from '../ImageUtils';
|
||||
import {
|
||||
saveImageToPathAsync,
|
||||
saveUrlToPathAsync,
|
||||
spawnAsyncThrowError,
|
||||
} from './ExponentTools';
|
||||
import logger from './Logger';
|
||||
import {
|
||||
AnyStandaloneContext,
|
||||
StandaloneContextService,
|
||||
StandaloneContextUser,
|
||||
} from './StandaloneContext';
|
||||
|
||||
function _getAppleIconQualifier(
|
||||
iconSize: number,
|
||||
iconResolution: number,
|
||||
): string {
|
||||
let iconQualifier;
|
||||
if (iconResolution !== 1) {
|
||||
// e.g. "29x29@3x"
|
||||
iconQualifier = `${iconSize}x${iconSize}@${iconResolution}x`;
|
||||
} else {
|
||||
iconQualifier = `${iconSize}x${iconSize}`;
|
||||
}
|
||||
if (iconSize === 76 || iconSize === 83.5) {
|
||||
// ipad sizes require ~ipad at the end
|
||||
iconQualifier = `${iconQualifier}~ipad`;
|
||||
}
|
||||
return iconQualifier;
|
||||
}
|
||||
|
||||
async function _saveDefaultIconToPathAsync(
|
||||
context: AnyStandaloneContext,
|
||||
filePath: string,
|
||||
) {
|
||||
if (context instanceof StandaloneContextUser) {
|
||||
const { data } = context;
|
||||
if (data.exp.icon) {
|
||||
await saveImageToPathAsync(data.projectPath, data.exp.icon, filePath);
|
||||
} else {
|
||||
throw new Error('Cannot save icon because app.json has no exp.icon key.');
|
||||
}
|
||||
} else {
|
||||
const { data } = context;
|
||||
if (data.manifest.ios && data.manifest.ios.iconUrl) {
|
||||
await saveUrlToPathAsync(data.manifest.ios.iconUrl, filePath);
|
||||
} else if (data.manifest.iconUrl) {
|
||||
await saveUrlToPathAsync(data.manifest.iconUrl, filePath);
|
||||
} else {
|
||||
throw new Error(
|
||||
'Cannot save icon because manifest has no iconUrl or ios.iconUrl key.',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Based on keys in the given context.config,
|
||||
* ensure that the proper iOS icon images exist -- assuming Info.plist already
|
||||
* points at them under CFBundleIcons.CFBundlePrimaryIcon.CFBundleIconFiles.
|
||||
*
|
||||
* This only works on MacOS (as far as I know) because it uses the sips utility.
|
||||
*/
|
||||
async function createAndWriteIconsToPathAsync(
|
||||
context: AnyStandaloneContext,
|
||||
destinationIconPath: string,
|
||||
) {
|
||||
let defaultIconFilename: string | null = 'exp-icon.png';
|
||||
try {
|
||||
await _saveDefaultIconToPathAsync(
|
||||
context,
|
||||
path.join(destinationIconPath, defaultIconFilename),
|
||||
);
|
||||
} catch (e) {
|
||||
defaultIconFilename = null;
|
||||
logger.warn(e.message);
|
||||
}
|
||||
|
||||
const iconSizes = [1024, 20, 29, 40, 60, 76, 83.5];
|
||||
|
||||
await Promise.all(
|
||||
iconSizes.map(async (iconSize) => {
|
||||
let iconResolutions;
|
||||
if (iconSize === 76) {
|
||||
// iPad has 1x and 2x icons for this size only
|
||||
iconResolutions = [1, 2];
|
||||
} else if (iconSize === 1024) {
|
||||
// marketing icon is weird
|
||||
iconResolutions = [1];
|
||||
} else if (iconSize === 83.5) {
|
||||
iconResolutions = [2];
|
||||
} else {
|
||||
iconResolutions = [2, 3];
|
||||
}
|
||||
|
||||
// We need to wait for all of these to finish!
|
||||
await Promise.all(
|
||||
iconResolutions.map(async (iconResolution) => {
|
||||
const iconQualifier = _getAppleIconQualifier(
|
||||
iconSize,
|
||||
iconResolution,
|
||||
);
|
||||
const iconKey = `iconUrl${iconQualifier}`;
|
||||
let rawIconFilename;
|
||||
let usesDefault = false;
|
||||
if (context instanceof StandaloneContextService) {
|
||||
// TODO(nikki): Support local paths for these icons
|
||||
const manifest = context.data.manifest;
|
||||
if (manifest.ios && manifest.ios.hasOwnProperty(iconKey)) {
|
||||
// manifest specifies an image just for this size/resolution, use that
|
||||
rawIconFilename = `exp-icon${iconQualifier}.png`;
|
||||
await saveUrlToPathAsync(
|
||||
manifest.ios[iconKey],
|
||||
`${destinationIconPath}/${rawIconFilename}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
if (!rawIconFilename) {
|
||||
// use default iconUrl
|
||||
usesDefault = true;
|
||||
if (defaultIconFilename) {
|
||||
rawIconFilename = defaultIconFilename;
|
||||
} else {
|
||||
logger.warn(
|
||||
`Project does not specify ios.${iconKey} nor a default iconUrl. Bundle will use the Expo logo.`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const iconFilename = `AppIcon${iconQualifier}.png`;
|
||||
const iconSizePx = iconSize * iconResolution;
|
||||
await spawnAsyncThrowError(
|
||||
'/bin/cp',
|
||||
[rawIconFilename, iconFilename],
|
||||
{
|
||||
stdio: 'inherit',
|
||||
cwd: destinationIconPath,
|
||||
},
|
||||
);
|
||||
try {
|
||||
await resizeImageAsync(
|
||||
iconSizePx,
|
||||
iconFilename,
|
||||
destinationIconPath,
|
||||
);
|
||||
} catch (e) {
|
||||
throw new Error(`Failed to resize image: ${iconFilename}. (${e})`);
|
||||
}
|
||||
|
||||
// reject non-square icons (because Apple will if we don't)
|
||||
const dims = await getImageDimensionsAsync(
|
||||
destinationIconPath,
|
||||
iconFilename,
|
||||
);
|
||||
if (!dims) {
|
||||
throw new Error(`Unable to read the dimensions of ${iconFilename}`);
|
||||
} else if (dims.width !== dims.height) {
|
||||
throw new Error(
|
||||
`iOS icons must be square, the dimensions of ${iconFilename} are ${dims}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (!usesDefault) {
|
||||
// non-default icon used, clean up the downloaded version
|
||||
await spawnAsyncThrowError('/bin/rm', [
|
||||
path.join(destinationIconPath, rawIconFilename),
|
||||
]);
|
||||
}
|
||||
}),
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
// clean up default icon
|
||||
if (defaultIconFilename) {
|
||||
await spawnAsyncThrowError('/bin/rm', [
|
||||
path.join(destinationIconPath, defaultIconFilename),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
export { createAndWriteIconsToPathAsync };
|
||||
@@ -0,0 +1,132 @@
|
||||
import fs from 'fs-extra';
|
||||
import difference from 'lodash/difference';
|
||||
import uuidv1 from 'uuid/v1';
|
||||
|
||||
import { createSpawner, spawnAsyncThrowError } from './ExponentTools';
|
||||
import _logger from './Logger';
|
||||
|
||||
export async function createKeychain(appUUID: string, saveResultToFile = true) {
|
||||
const BUILD_PHASE = 'creating keychain';
|
||||
const logger = _logger.withFields({ buildPhase: BUILD_PHASE });
|
||||
const spawn = createSpawner(BUILD_PHASE, logger);
|
||||
|
||||
const name = uuidv1();
|
||||
const password = uuidv1();
|
||||
const path = getKeychainPath(name);
|
||||
|
||||
logger.info('creating new keychain...');
|
||||
await runFastlane([
|
||||
'run',
|
||||
'create_keychain',
|
||||
`path:${path}`,
|
||||
`password:${password}`,
|
||||
'unlock:true',
|
||||
'timeout:360000',
|
||||
]);
|
||||
await spawn('security', 'show-keychain-info', path, { stdoutOnly: true });
|
||||
|
||||
logger.info('created new keychain');
|
||||
const keychainInfo = {
|
||||
name,
|
||||
path,
|
||||
password,
|
||||
};
|
||||
|
||||
if (saveResultToFile) {
|
||||
const keychainInfoPath = getKeychainInfoPath(appUUID);
|
||||
await fs.writeFile(keychainInfoPath, JSON.stringify(keychainInfo));
|
||||
logger.info('saved keychain info to %s', keychainInfoPath);
|
||||
}
|
||||
|
||||
return keychainInfo;
|
||||
}
|
||||
|
||||
export async function deleteKeychain({ path, appUUID }: { path: string; appUUID?: string }) {
|
||||
const BUILD_PHASE = 'deleting keychain';
|
||||
const logger = _logger.withFields({ buildPhase: BUILD_PHASE });
|
||||
|
||||
logger.info('deleting keychain...');
|
||||
await runFastlane(['run', 'delete_keychain', `keychain_path:${path}`]);
|
||||
|
||||
if (appUUID) {
|
||||
const keychainInfoPath = getKeychainInfoPath(appUUID);
|
||||
await fs.remove(keychainInfoPath);
|
||||
}
|
||||
}
|
||||
|
||||
export async function importIntoKeychain({
|
||||
keychainPath,
|
||||
certPath,
|
||||
certPassword,
|
||||
}: {
|
||||
keychainPath: string;
|
||||
certPath: string;
|
||||
certPassword?: string;
|
||||
}) {
|
||||
const BUILD_PHASE = 'importing certificate into keychain';
|
||||
const logger = _logger.withFields({ buildPhase: BUILD_PHASE });
|
||||
const spawn = createSpawner(BUILD_PHASE);
|
||||
|
||||
logger.info('importing certificate into keychain...');
|
||||
const args = ['import', certPath, '-A', '-k', keychainPath, '-f', 'pkcs12'];
|
||||
if (certPassword) {
|
||||
logger.info('certificate has password');
|
||||
args.push('-P', certPassword);
|
||||
} else {
|
||||
logger.info('certificate doesn\'t have password');
|
||||
}
|
||||
await spawn('security', ...args);
|
||||
logger.info('imported certificate into keychain');
|
||||
}
|
||||
|
||||
export async function cleanUpKeychains() {
|
||||
try {
|
||||
_logger.info('Cleaning up keychains...');
|
||||
const { stdout: keychainsListRaw } = await spawnAsyncThrowError(
|
||||
'security',
|
||||
['list-keychains'],
|
||||
{ stdio: 'pipe' },
|
||||
);
|
||||
const allKeychainsList = (keychainsListRaw.match(/"(.*)"/g) || []).map((i) =>
|
||||
i.slice(1, i.length - 1),
|
||||
);
|
||||
const turtleKeychainsList = keychainsListRaw.match(/\/private\/tmp\/xdl\/(.*).keychain/g);
|
||||
let shouldCleanSearchList = false;
|
||||
if (turtleKeychainsList) {
|
||||
for (const keychainPath of turtleKeychainsList) {
|
||||
try {
|
||||
await deleteKeychain({ path: keychainPath });
|
||||
} catch (err) {
|
||||
_logger.warn(`Failed to delete keychain: ${keychainPath}`, err);
|
||||
shouldCleanSearchList = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldCleanSearchList) {
|
||||
const newSearchList = difference(allKeychainsList, turtleKeychainsList);
|
||||
await spawnAsyncThrowError('security', ['list-keychains', '-s', ...newSearchList], {
|
||||
stdio: 'pipe',
|
||||
});
|
||||
}
|
||||
}
|
||||
_logger.info('Cleaned up keychains');
|
||||
} catch (err) {
|
||||
_logger.error(err);
|
||||
throw new Error('Failed to clean up keychains');
|
||||
}
|
||||
}
|
||||
|
||||
async function runFastlane(fastlaneArgs: string[]) {
|
||||
const fastlaneEnvVars = {
|
||||
FASTLANE_DISABLE_COLORS: '1',
|
||||
FASTLANE_SKIP_UPDATE_CHECK: '1',
|
||||
CI: '1',
|
||||
LC_ALL: 'en_US.UTF-8',
|
||||
};
|
||||
await spawnAsyncThrowError('fastlane', fastlaneArgs, {
|
||||
env: { ...process.env, ...fastlaneEnvVars },
|
||||
});
|
||||
}
|
||||
|
||||
const getKeychainPath = (name: string) => `/private/tmp/xdl/${name}.keychain`;
|
||||
const getKeychainInfoPath = (appUUID: string) => `/private/tmp/${appUUID}-keychain-info.json`;
|
||||
@@ -0,0 +1,251 @@
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
import rimraf from 'rimraf';
|
||||
import { DOMParser, XMLSerializer } from 'xmldom';
|
||||
|
||||
import {
|
||||
manifestUsesSplashApi,
|
||||
saveImageToPathAsync,
|
||||
saveUrlToPathAsync,
|
||||
spawnAsyncThrowError,
|
||||
transformFileContentsAsync,
|
||||
} from './ExponentTools';
|
||||
import * as IosWorkspace from './IosWorkspace';
|
||||
import _logger from './Logger';
|
||||
|
||||
const logger = _logger.withFields({ buildPhase: 'configuring NSBundle' });
|
||||
|
||||
const ASPECT_FILL = 'scaleAspectFill';
|
||||
const ASPECT_FIT = 'scaleAspectFit';
|
||||
|
||||
const backgroundImageViewID = 'Bsh-cT-K4l';
|
||||
const backgroundViewID = 'OfY-5Y-tS4';
|
||||
|
||||
function _backgroundColorFromHexString(hexColor) {
|
||||
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hexColor);
|
||||
if (!result || result.length < 4) {
|
||||
// Default to white if we can't parse the color. We should have 3 matches.
|
||||
logger.warn('Unable to parse color: ', hexColor, ' result:', result);
|
||||
return { r: 1, g: 1, b: 1 };
|
||||
}
|
||||
|
||||
const r = parseInt(result[1], 16) / 255;
|
||||
const g = parseInt(result[2], 16) / 255;
|
||||
const b = parseInt(result[3], 16) / 255;
|
||||
return { r, g, b };
|
||||
}
|
||||
|
||||
function _setBackgroundColor(manifest, dom) {
|
||||
let backgroundColorString;
|
||||
if (manifest.ios && manifest.ios.splash && manifest.ios.splash.backgroundColor) {
|
||||
backgroundColorString = manifest.ios.splash.backgroundColor;
|
||||
} else if (manifest.splash && manifest.splash.backgroundColor) {
|
||||
backgroundColorString = manifest.splash.backgroundColor;
|
||||
}
|
||||
|
||||
// Default to white
|
||||
if (!backgroundColorString) {
|
||||
backgroundColorString = '#FFFFFF';
|
||||
}
|
||||
|
||||
const { r, g, b } = _backgroundColorFromHexString(backgroundColorString);
|
||||
const backgroundViewNode = dom.getElementById(backgroundViewID);
|
||||
const backgroundViewColorNodes = backgroundViewNode.getElementsByTagName('color');
|
||||
let backgroundColorNode;
|
||||
for (let i = 0; i < backgroundViewColorNodes.length; i++) {
|
||||
const node = backgroundViewColorNodes[i];
|
||||
if (node.parentNode.getAttribute('id') !== backgroundViewID) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (node.getAttribute('key') === 'backgroundColor') {
|
||||
backgroundColorNode = node;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (backgroundColorNode) {
|
||||
backgroundColorNode.setAttribute('red', r);
|
||||
backgroundColorNode.setAttribute('green', g);
|
||||
backgroundColorNode.setAttribute('blue', b);
|
||||
}
|
||||
}
|
||||
|
||||
async function _saveImageAssetsAsync(context) {
|
||||
let tabletImagePathOrUrl, phoneImagePathOrUrl;
|
||||
|
||||
if (context.type === 'user') {
|
||||
// copy images from local project
|
||||
const exp = context.data.exp;
|
||||
if (exp.ios && exp.ios.splash && exp.ios.splash.image) {
|
||||
phoneImagePathOrUrl = exp.ios.splash.image;
|
||||
|
||||
if (exp.ios.splash.tabletImage) {
|
||||
tabletImagePathOrUrl = exp.ios.splash.tabletImage;
|
||||
}
|
||||
} else if (exp.splash && exp.splash.image) {
|
||||
phoneImagePathOrUrl = exp.splash.image;
|
||||
}
|
||||
} else {
|
||||
// use uploaded assets from published project
|
||||
const manifest = context.data.manifest;
|
||||
if (manifest.ios && manifest.ios.splash && manifest.ios.splash.imageUrl) {
|
||||
phoneImagePathOrUrl = manifest.ios.splash.imageUrl;
|
||||
|
||||
if (manifest.ios.splash.tabletImageUrl) {
|
||||
tabletImagePathOrUrl = manifest.ios.splash.tabletImageUrl;
|
||||
}
|
||||
} else if (manifest.splash && manifest.splash.imageUrl) {
|
||||
phoneImagePathOrUrl = manifest.splash.imageUrl;
|
||||
}
|
||||
}
|
||||
|
||||
if (!phoneImagePathOrUrl) {
|
||||
return;
|
||||
}
|
||||
|
||||
const outputs = [];
|
||||
if (!tabletImagePathOrUrl) {
|
||||
outputs.push({
|
||||
pathOrUrl: phoneImagePathOrUrl,
|
||||
filename: 'launch_background_image.png',
|
||||
});
|
||||
} else {
|
||||
outputs.push({
|
||||
pathOrUrl: phoneImagePathOrUrl,
|
||||
filename: 'launch_background_image~iphone.png',
|
||||
});
|
||||
outputs.push({
|
||||
pathOrUrl: tabletImagePathOrUrl,
|
||||
filename: 'launch_background_image.png',
|
||||
});
|
||||
}
|
||||
|
||||
const { supportingDirectory } = IosWorkspace.getPaths(context);
|
||||
const projectRoot = context.type === 'user' ? context.data.projectPath : supportingDirectory;
|
||||
outputs.forEach(async output => {
|
||||
const { pathOrUrl, filename } = output;
|
||||
const destinationPath = path.join(supportingDirectory, filename);
|
||||
await saveImageToPathAsync(projectRoot, pathOrUrl, destinationPath);
|
||||
});
|
||||
}
|
||||
|
||||
function _setBackgroundImageResizeMode(manifest, dom) {
|
||||
const backgroundViewMode = (() => {
|
||||
let mode;
|
||||
if (!manifest) {
|
||||
return ASPECT_FIT;
|
||||
}
|
||||
|
||||
if (manifest.ios && manifest.ios.splash && manifest.ios.splash.resizeMode) {
|
||||
mode = manifest.ios.splash.resizeMode;
|
||||
} else if (manifest.splash && manifest.splash.resizeMode) {
|
||||
mode = manifest.splash.resizeMode;
|
||||
}
|
||||
|
||||
return mode === 'cover' ? ASPECT_FILL : ASPECT_FIT;
|
||||
})();
|
||||
|
||||
const backgroundImageViewNode = dom.getElementById(backgroundImageViewID);
|
||||
if (backgroundImageViewNode) {
|
||||
backgroundImageViewNode.setAttribute('contentMode', backgroundViewMode);
|
||||
}
|
||||
}
|
||||
|
||||
async function _copyIntermediateLaunchScreenAsync(context, launchScreenPath) {
|
||||
let splashTemplateFilename;
|
||||
if (context.type === 'user') {
|
||||
const { supportingDirectory } = IosWorkspace.getPaths(context);
|
||||
splashTemplateFilename = path.join(supportingDirectory, 'LaunchScreen.xib');
|
||||
} else {
|
||||
// TODO: after shell apps use detached workspaces,
|
||||
// we can just do this with the workspace's copy instead of referencing expoSourcePath.
|
||||
const expoTemplatePath = path.join(
|
||||
context.data.expoSourcePath,
|
||||
'..',
|
||||
'exponent-view-template',
|
||||
'ios'
|
||||
);
|
||||
splashTemplateFilename = path.join(
|
||||
expoTemplatePath,
|
||||
'exponent-view-template',
|
||||
'Supporting',
|
||||
'LaunchScreen.xib'
|
||||
);
|
||||
}
|
||||
await spawnAsyncThrowError('/bin/cp', [splashTemplateFilename, launchScreenPath], {
|
||||
stdio: 'inherit',
|
||||
});
|
||||
}
|
||||
|
||||
async function configureLaunchAssetsAsync(context, intermediatesDirectory) {
|
||||
logger.info('Configuring iOS Launch Screen...');
|
||||
|
||||
fs.mkdirpSync(intermediatesDirectory);
|
||||
const { supportingDirectory } = IosWorkspace.getPaths(context);
|
||||
const config = context.config;
|
||||
|
||||
const splashIntermediateFilename = path.join(intermediatesDirectory, 'LaunchScreen.xib');
|
||||
|
||||
let xibFile = null;
|
||||
if (context.type === 'user') {
|
||||
xibFile =
|
||||
context.data.exp.ios && context.data.exp.ios.splash && context.data.exp.ios.splash.xib;
|
||||
} else {
|
||||
xibFile =
|
||||
context.data.manifest.ios &&
|
||||
context.data.manifest.ios.splash &&
|
||||
context.data.manifest.ios.splash.xibUrl;
|
||||
}
|
||||
if (xibFile) {
|
||||
if (context.type === 'user') {
|
||||
const sourcePath = path.resolve(context.data.projectPath, xibFile);
|
||||
await spawnAsyncThrowError('/bin/cp', [sourcePath, splashIntermediateFilename], {
|
||||
stdio: 'inherit',
|
||||
});
|
||||
} else {
|
||||
await saveUrlToPathAsync(xibFile, splashIntermediateFilename);
|
||||
}
|
||||
} else {
|
||||
await _copyIntermediateLaunchScreenAsync(context, splashIntermediateFilename);
|
||||
|
||||
if (manifestUsesSplashApi(config, 'ios')) {
|
||||
await transformFileContentsAsync(splashIntermediateFilename, fileString => {
|
||||
const parser = new DOMParser();
|
||||
const serializer = new XMLSerializer();
|
||||
const dom = parser.parseFromString(fileString);
|
||||
|
||||
_setBackgroundColor(config, dom);
|
||||
_setBackgroundImageResizeMode(config, dom);
|
||||
|
||||
return serializer.serializeToString(dom);
|
||||
});
|
||||
|
||||
await _saveImageAssetsAsync(context);
|
||||
}
|
||||
}
|
||||
|
||||
if (context.type === 'user') {
|
||||
await spawnAsyncThrowError(
|
||||
'/bin/cp',
|
||||
[splashIntermediateFilename, path.join(supportingDirectory, 'LaunchScreen.xib')],
|
||||
{
|
||||
stdio: 'inherit',
|
||||
}
|
||||
);
|
||||
} else {
|
||||
const splashOutputFilename = path.join(supportingDirectory, 'Base.lproj', 'LaunchScreen.nib');
|
||||
await spawnAsyncThrowError('ibtool', [
|
||||
'--compile',
|
||||
splashOutputFilename,
|
||||
splashIntermediateFilename,
|
||||
]);
|
||||
|
||||
const generatedUnnecessaryNib = path.join(supportingDirectory, 'LaunchScreen.nib');
|
||||
if (fs.existsSync(generatedUnnecessaryNib)) {
|
||||
rimraf.sync(generatedUnnecessaryNib);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export { configureLaunchAssetsAsync };
|
||||
@@ -0,0 +1,27 @@
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
|
||||
import { getResolvedLocalesAsync } from './ExponentTools';
|
||||
|
||||
export async function writeLocalizationResourcesAsync({
|
||||
projectRoot,
|
||||
supportingDirectory,
|
||||
context,
|
||||
}) {
|
||||
let locales = {};
|
||||
if (context.type === 'user') {
|
||||
locales = await getResolvedLocalesAsync(projectRoot, context.config);
|
||||
} else if (context.type === 'service') {
|
||||
locales = context.config.locales !== undefined ? context.config.locales : {};
|
||||
}
|
||||
for (const [lang, localizationObj] of Object.entries(locales)) {
|
||||
const dir = path.join(supportingDirectory, `${lang}.lproj`);
|
||||
fs.mkdirpSync(dir);
|
||||
const strings = path.join(dir, 'InfoPlist.strings');
|
||||
const buffer = [];
|
||||
for (const [plistKey, localVersion] of Object.entries(localizationObj)) {
|
||||
buffer.push(`${plistKey} = "${localVersion}";`);
|
||||
}
|
||||
await fs.writeFile(strings, buffer.join('\n'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,760 @@
|
||||
import { ExpoConfig } from '@expo/config';
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
|
||||
import turtleLogger from 'turtle/logger';
|
||||
import * as AssetBundle from './AssetBundle';
|
||||
import {
|
||||
getBundleFileNameForSdkVersion,
|
||||
getManifestAsync,
|
||||
getManifestFileNameForSdkVersion,
|
||||
manifestUsesSplashApi,
|
||||
parseSdkMajorVersion,
|
||||
saveUrlToPathAsync,
|
||||
} from './ExponentTools';
|
||||
import * as IosAssetArchive from './IosAssetArchive';
|
||||
import * as IosIcons from './IosIcons';
|
||||
// @ts-ignore: No TS support
|
||||
import * as IosLaunchScreen from './IosLaunchScreen';
|
||||
// @ts-ignore: No TS support
|
||||
import * as IosLocalization from './IosLocalization';
|
||||
import * as IosPlist from './IosPlist';
|
||||
// @ts-ignore: No TS support
|
||||
import * as IosWorkspace from './IosWorkspace';
|
||||
import logger from './Logger';
|
||||
import {
|
||||
AnyStandaloneContext,
|
||||
isStandaloneContextDataService,
|
||||
StandaloneContextService,
|
||||
StandaloneContextUser,
|
||||
} from './StandaloneContext';
|
||||
|
||||
// TODO: move this somewhere else. this is duplicated in universe/exponent/template-files/keys,
|
||||
// but xdl doesn't have access to that.
|
||||
const DEFAULT_FABRIC_KEY = '81130e95ea13cd7ed9a4f455e96214902c721c99';
|
||||
const DEFAULT_GAD_APPLICATION_ID = 'ca-app-pub-3940256099942544~1458002511';
|
||||
const KERNEL_URL = 'https://expo.io/@exponent/home';
|
||||
|
||||
function _configureInfoPlistForLocalDevelopment(
|
||||
config: any,
|
||||
exp: ExpoConfig,
|
||||
): ExpoConfig {
|
||||
// add detached scheme
|
||||
if (exp.isDetached && exp.detach?.scheme) {
|
||||
if (!config.CFBundleURLTypes) {
|
||||
config.CFBundleURLTypes = [
|
||||
{
|
||||
CFBundleURLSchemes: [],
|
||||
},
|
||||
];
|
||||
}
|
||||
config.CFBundleURLTypes[0].CFBundleURLSchemes.push(exp.detach.scheme);
|
||||
}
|
||||
// for local dev, don't specify device family here
|
||||
if (config.UIDeviceFamily) {
|
||||
delete config.UIDeviceFamily;
|
||||
}
|
||||
return config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prints warnings or info about the configured environment for local development.
|
||||
*/
|
||||
function _logDeveloperInfoForLocalDevelopment(infoPlist: any[]): void {
|
||||
// warn about *UsageDescription changes
|
||||
const usageKeysConfigured = [];
|
||||
for (const key in infoPlist) {
|
||||
if (key in infoPlist && key.includes('UsageDescription')) {
|
||||
usageKeysConfigured.push(key);
|
||||
}
|
||||
}
|
||||
if (usageKeysConfigured.length) {
|
||||
logger.info(
|
||||
'We added some permissions keys to `Info.plist` in your detached iOS project:',
|
||||
);
|
||||
usageKeysConfigured.forEach((key) => {
|
||||
logger.info(` ${key}`);
|
||||
});
|
||||
logger.info(
|
||||
'You may want to revise them to include language appropriate to your project. ' +
|
||||
'You can also remove them if your app will never use the corresponding API. ' +
|
||||
'See the Apple docs for these keys.',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function _cleanPropertyListBackupsAsync(
|
||||
context: AnyStandaloneContext,
|
||||
backupPath: string,
|
||||
): Promise<void> {
|
||||
if (context.build?.ios?.buildType !== 'client') {
|
||||
await IosPlist.cleanBackupAsync(backupPath, 'EXShell', false);
|
||||
// [dsokal] it's probably ok to clean up those two for client but no one knows if it's true for sure
|
||||
await IosPlist.cleanBackupAsync(backupPath, 'EXSDKVersions', false);
|
||||
await IosPlist.cleanBackupAsync(backupPath, 'EXBuildConstants', false);
|
||||
}
|
||||
await IosPlist.cleanBackupAsync(backupPath, 'Info', false);
|
||||
// TODO: support this in user contexts as well
|
||||
if (context instanceof StandaloneContextService) {
|
||||
const { projectName } = IosWorkspace.getPaths(context);
|
||||
await IosPlist.cleanBackupAsync(
|
||||
backupPath,
|
||||
`${projectName}.entitlements`,
|
||||
false,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Write the manifest and JS bundle to the NSBundle.
|
||||
*/
|
||||
async function _preloadManifestAndBundleAsync(
|
||||
manifest: { bundleUrl: string; [key: string]: any },
|
||||
supportingDirectory: string,
|
||||
manifestFilename: string,
|
||||
bundleFilename: string,
|
||||
): Promise<void> {
|
||||
const bundleUrl = manifest.bundleUrl;
|
||||
await fs.writeFile(
|
||||
path.join(supportingDirectory, manifestFilename),
|
||||
JSON.stringify(manifest),
|
||||
);
|
||||
await saveUrlToPathAsync(
|
||||
bundleUrl,
|
||||
path.join(supportingDirectory, bundleFilename),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* This method only makes sense when operating on a context with sdk version < 26.
|
||||
*/
|
||||
async function _maybeLegacyPreloadKernelManifestAndBundleAsync(
|
||||
context: AnyStandaloneContext,
|
||||
manifestFilename: string,
|
||||
bundleFilename: string,
|
||||
): Promise<void> {
|
||||
const { supportingDirectory } = IosWorkspace.getPaths(context);
|
||||
const sdkVersionSupported = await IosWorkspace.getNewestSdkVersionSupportedAsync(
|
||||
context,
|
||||
);
|
||||
|
||||
if (parseSdkMajorVersion(sdkVersionSupported) < 26) {
|
||||
logger.info('Preloading Expo kernel JS...');
|
||||
const kernelManifest = await getManifestAsync(KERNEL_URL, {
|
||||
'Exponent-SDK-Version': sdkVersionSupported,
|
||||
'Exponent-Platform': 'ios',
|
||||
'Accept': 'application/expo+json,application/json',
|
||||
});
|
||||
return _preloadManifestAndBundleAsync(
|
||||
kernelManifest,
|
||||
supportingDirectory,
|
||||
manifestFilename,
|
||||
bundleFilename,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure a standalone entitlements file.
|
||||
*/
|
||||
async function _configureEntitlementsAsync(
|
||||
context: AnyStandaloneContext,
|
||||
): Promise<any> {
|
||||
if (context instanceof StandaloneContextUser) {
|
||||
// don't modify .entitlements, print info/instructions
|
||||
const { exp } = context.data;
|
||||
logger.info(
|
||||
'Your iOS ExpoKit project will not contain an .entitlements file by default. ' +
|
||||
'If you need specific Apple entitlements, enable them manually via Xcode or the Apple Developer website.',
|
||||
);
|
||||
const keysToFlag = [];
|
||||
if (exp.ios?.usesIcloudStorage) {
|
||||
keysToFlag.push('ios.usesIcloudStorage');
|
||||
}
|
||||
if (exp.ios?.associatedDomains) {
|
||||
keysToFlag.push('ios.associatedDomains');
|
||||
}
|
||||
if (keysToFlag.length) {
|
||||
logger.info(
|
||||
'We noticed the following keys in your project which may require entitlements:',
|
||||
);
|
||||
keysToFlag.forEach((key) => {
|
||||
logger.info(` ${key}`);
|
||||
});
|
||||
}
|
||||
return {};
|
||||
} else {
|
||||
// modify the .entitlements file
|
||||
const { projectName, supportingDirectory } = IosWorkspace.getPaths(context);
|
||||
const manifest =
|
||||
isStandaloneContextDataService(context.data) && context.data.manifest;
|
||||
const entitlementsFilename = `${projectName}.entitlements`;
|
||||
const { appleTeamId } = context.build.ios || ({} as any);
|
||||
if (!fs.existsSync(path.join(supportingDirectory, entitlementsFilename))) {
|
||||
await IosPlist.createBlankAsync(
|
||||
supportingDirectory,
|
||||
entitlementsFilename,
|
||||
);
|
||||
}
|
||||
const result = IosPlist.modifyAsync(
|
||||
supportingDirectory,
|
||||
entitlementsFilename,
|
||||
(entitlements) => {
|
||||
if (manifest.ios?.entitlements) {
|
||||
for (const key in manifest.ios.entitlements) {
|
||||
if (manifest.ios.entitlements.hasOwnProperty(key)) {
|
||||
entitlements[key] = manifest.ios.entitlements[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// push notif entitlement changes based on build configuration
|
||||
entitlements['aps-environment'] =
|
||||
context.build.configuration === 'Release'
|
||||
? 'production'
|
||||
: 'development';
|
||||
|
||||
// remove iCloud-specific entitlements if the developer isn't using iCloud Storage with DocumentPicker
|
||||
if (manifest.ios?.usesIcloudStorage && appleTeamId) {
|
||||
entitlements['com.apple.developer.icloud-container-identifiers'] = [
|
||||
'iCloud.' + manifest.ios.bundleIdentifier,
|
||||
];
|
||||
entitlements['com.apple.developer.ubiquity-container-identifiers'] = [
|
||||
'iCloud.' + manifest.ios.bundleIdentifier,
|
||||
];
|
||||
entitlements['com.apple.developer.ubiquity-kvstore-identifier'] =
|
||||
appleTeamId + '.' + manifest.ios.bundleIdentifier;
|
||||
entitlements['com.apple.developer.icloud-services'] = [
|
||||
'CloudDocuments',
|
||||
];
|
||||
} else {
|
||||
[
|
||||
'com.apple.developer.icloud-container-identifiers',
|
||||
'com.apple.developer.icloud-services',
|
||||
'com.apple.developer.ubiquity-container-identifiers',
|
||||
'com.apple.developer.ubiquity-kvstore-identifier',
|
||||
].forEach((key) => {
|
||||
if (entitlements.hasOwnProperty(key)) {
|
||||
delete entitlements[key];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (manifest.ios?.usesAppleSignIn) {
|
||||
entitlements['com.apple.developer.applesignin'] = ['Default'];
|
||||
} else if (
|
||||
entitlements.hasOwnProperty('com.apple.developer.applesignin')
|
||||
) {
|
||||
delete entitlements['com.apple.developer.applesignin'];
|
||||
}
|
||||
|
||||
if (manifest.ios?.accessesContactNotes) {
|
||||
entitlements['com.apple.developer.contacts.notes'] =
|
||||
manifest.ios.accessesContactNotes;
|
||||
} else if (
|
||||
entitlements.hasOwnProperty('com.apple.developer.contacts.notes')
|
||||
) {
|
||||
delete entitlements['com.apple.developer.contacts.notes'];
|
||||
}
|
||||
|
||||
// Add app associated domains remove exp-specific ones.
|
||||
if (manifest.ios?.associatedDomains) {
|
||||
entitlements['com.apple.developer.associated-domains'] =
|
||||
manifest.ios.associatedDomains;
|
||||
} else if (
|
||||
entitlements.hasOwnProperty('com.apple.developer.associated-domains')
|
||||
) {
|
||||
delete entitlements['com.apple.developer.associated-domains'];
|
||||
}
|
||||
|
||||
// for now, remove any merchant ID in shell apps
|
||||
// (TODO: better plan for payments)
|
||||
delete entitlements['com.apple.developer.in-app-payments'];
|
||||
|
||||
return entitlements;
|
||||
},
|
||||
);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the private config for a project.
|
||||
* For standalone apps, this is copied into a separate context field context.data.privateConfig
|
||||
* by the turtle builder. For a local project, this is available in app.json under ios.config.
|
||||
*/
|
||||
function _getPrivateConfig(
|
||||
context: AnyStandaloneContext,
|
||||
): { [key: string]: any } | undefined {
|
||||
if (context instanceof StandaloneContextService) {
|
||||
return context.data.privateConfig;
|
||||
} else {
|
||||
const exp = context.data.exp;
|
||||
if (exp && exp.ios) {
|
||||
return exp.ios.config;
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function _isAppleUsageDescriptionKey(key: string): boolean {
|
||||
return key.includes('UsageDescription');
|
||||
}
|
||||
|
||||
function ensureStringArray(input: any): string[] {
|
||||
// Normalize schemes and filter invalid schemes.
|
||||
const stringArray = (Array.isArray(input) ? input : [input]).filter(
|
||||
(scheme) => typeof scheme === 'string' && !!scheme,
|
||||
);
|
||||
return stringArray;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure an iOS Info.plist for a standalone app.
|
||||
*/
|
||||
async function _configureInfoPlistAsync(
|
||||
context: AnyStandaloneContext,
|
||||
): Promise<void> {
|
||||
const { supportingDirectory } = IosWorkspace.getPaths(context);
|
||||
const config = context.config;
|
||||
const privateConfig = _getPrivateConfig(context);
|
||||
|
||||
const result = await IosPlist.modifyAsync(
|
||||
supportingDirectory,
|
||||
'Info',
|
||||
(infoPlist) => {
|
||||
// make sure this happens first:
|
||||
// apply any custom information from ios.infoPlist prior to all other exponent config
|
||||
const usageDescriptionKeysConfigured: { [key: string]: any } = {};
|
||||
let extraConfig;
|
||||
if (config.ios && config.ios.infoPlist) {
|
||||
extraConfig = config.ios.infoPlist;
|
||||
for (const key in extraConfig) {
|
||||
if (extraConfig.hasOwnProperty(key)) {
|
||||
infoPlist[key] = extraConfig[key];
|
||||
|
||||
// if the user provides *UsageDescription keys, don't override them later.
|
||||
if (_isAppleUsageDescriptionKey(key)) {
|
||||
usageDescriptionKeysConfigured[key] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// reset the status bar style to the default gray. this can be removed if we
|
||||
// ever change expo client to use `UIStatusBarStyleDefault` instead of
|
||||
// `UIStatusBarStyleLightContent`
|
||||
if (!extraConfig?.UIStatusBarStyle) {
|
||||
infoPlist.UIStatusBarStyle = 'UIStatusBarStyleDefault';
|
||||
}
|
||||
|
||||
// bundle id
|
||||
infoPlist.CFBundleIdentifier = config?.ios?.bundleIdentifier ?? null;
|
||||
if (!infoPlist.CFBundleIdentifier) {
|
||||
throw new Error(
|
||||
`Cannot configure an iOS app with no bundle identifier.`,
|
||||
);
|
||||
}
|
||||
|
||||
// app name
|
||||
infoPlist.CFBundleName = config.name;
|
||||
infoPlist.CFBundleDisplayName = context.build.isExpoClientBuild()
|
||||
? 'Expo (Custom)'
|
||||
: config.name;
|
||||
|
||||
// determine app linking schemes
|
||||
const multiPlatformSchemes = ensureStringArray(config.scheme);
|
||||
const iosSchemes = ensureStringArray(config.ios?.scheme);
|
||||
const linkingSchemes = [...multiPlatformSchemes, ...iosSchemes];
|
||||
if (config.facebookScheme && config.facebookScheme.startsWith('fb')) {
|
||||
linkingSchemes.push(config.facebookScheme);
|
||||
}
|
||||
|
||||
if (
|
||||
privateConfig &&
|
||||
privateConfig.googleSignIn &&
|
||||
privateConfig.googleSignIn.reservedClientId
|
||||
) {
|
||||
linkingSchemes.push(privateConfig.googleSignIn.reservedClientId);
|
||||
}
|
||||
|
||||
// remove exp scheme, add app scheme(s)
|
||||
infoPlist.CFBundleURLTypes = [
|
||||
{
|
||||
CFBundleURLSchemes: linkingSchemes,
|
||||
},
|
||||
{
|
||||
// Add the generic oauth redirect, it's important that it has the name
|
||||
// 'OAuthRedirect' so we can find it in app code.
|
||||
CFBundleURLName: 'OAuthRedirect',
|
||||
CFBundleURLSchemes: [infoPlist.CFBundleIdentifier],
|
||||
},
|
||||
];
|
||||
|
||||
// add or remove other facebook config
|
||||
if (config.facebookAppId) {
|
||||
infoPlist.FacebookAppID = config.facebookAppId;
|
||||
let queriesSchemes = infoPlist.LSApplicationQueriesSchemes || [];
|
||||
queriesSchemes = queriesSchemes.concat([
|
||||
'fbapi',
|
||||
'fb-messenger-api',
|
||||
'fbauth2',
|
||||
'fbshareextension',
|
||||
]);
|
||||
infoPlist.LSApplicationQueriesSchemes = queriesSchemes;
|
||||
} else {
|
||||
delete infoPlist.FacebookAppID;
|
||||
}
|
||||
if (config.facebookDisplayName) {
|
||||
infoPlist.FacebookDisplayName = config.facebookDisplayName;
|
||||
} else {
|
||||
delete infoPlist.FacebookDisplayName;
|
||||
}
|
||||
|
||||
if (parseSdkMajorVersion(config.sdkVersion) >= 36) {
|
||||
infoPlist.FacebookAutoInitEnabled =
|
||||
config.facebookAutoInitEnabled || false;
|
||||
infoPlist.FacebookAutoLogAppEventsEnabled =
|
||||
config.facebookAutoLogAppEventsEnabled || false;
|
||||
infoPlist.FacebookAdvertiserIDCollectionEnabled =
|
||||
config.facebookAdvertiserIDCollectionEnabled || false;
|
||||
}
|
||||
|
||||
// set ITSAppUsesNonExemptEncryption to let people skip manually
|
||||
// entering it in iTunes Connect
|
||||
if (
|
||||
privateConfig &&
|
||||
privateConfig.hasOwnProperty('usesNonExemptEncryption') &&
|
||||
privateConfig.usesNonExemptEncryption === false
|
||||
) {
|
||||
infoPlist.ITSAppUsesNonExemptEncryption = false;
|
||||
}
|
||||
|
||||
// google maps api key
|
||||
if (privateConfig && privateConfig.googleMapsApiKey) {
|
||||
infoPlist.GMSApiKey = privateConfig.googleMapsApiKey;
|
||||
}
|
||||
|
||||
// Google Mobile Ads App ID
|
||||
// The app crashes if the app ID isn't provided, so if the user
|
||||
// doesn't provide the ID, we leave the sample one.
|
||||
infoPlist.GADApplicationIdentifier =
|
||||
(privateConfig && privateConfig.googleMobileAdsAppId) ||
|
||||
DEFAULT_GAD_APPLICATION_ID;
|
||||
|
||||
// Auto-init of Google App Measurement
|
||||
// unless the user explicitly specifies they want to auto-init, we set delay to true
|
||||
infoPlist.GADDelayAppMeasurementInit = !(
|
||||
privateConfig && privateConfig.googleMobileAdsAutoInit
|
||||
);
|
||||
|
||||
// use version from manifest
|
||||
const version = config.version ? config.version : '0.0.0';
|
||||
const buildNumber =
|
||||
config.ios && config.ios.buildNumber ? config.ios.buildNumber : '1';
|
||||
infoPlist.CFBundleShortVersionString = version;
|
||||
infoPlist.CFBundleVersion = buildNumber;
|
||||
|
||||
infoPlist.Fabric = {
|
||||
APIKey:
|
||||
(privateConfig &&
|
||||
privateConfig.fabric &&
|
||||
privateConfig.fabric.apiKey) ||
|
||||
DEFAULT_FABRIC_KEY,
|
||||
Kits: [
|
||||
{
|
||||
KitInfo: {},
|
||||
KitName: 'Crashlytics',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
if (privateConfig && privateConfig.branch) {
|
||||
infoPlist.branch_key = {
|
||||
live: privateConfig.branch.apiKey,
|
||||
};
|
||||
}
|
||||
|
||||
const permissionsAppName = config.name ? config.name : 'this app';
|
||||
for (const key in infoPlist) {
|
||||
if (
|
||||
infoPlist.hasOwnProperty(key) &&
|
||||
_isAppleUsageDescriptionKey(key) &&
|
||||
!usageDescriptionKeysConfigured.hasOwnProperty(key)
|
||||
) {
|
||||
infoPlist[key] = infoPlist[key].replace(
|
||||
'Expo experiences',
|
||||
permissionsAppName,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 1 is iPhone, 2 is iPad
|
||||
infoPlist.UIDeviceFamily =
|
||||
config.ios && config.ios.supportsTablet ? [1, 2] : [1];
|
||||
|
||||
// allow iPad-only
|
||||
if (config.ios && config.ios.isTabletOnly) {
|
||||
infoPlist.UIDeviceFamily = [2];
|
||||
}
|
||||
|
||||
// Whether requires full screen on iPad
|
||||
infoPlist.UIRequiresFullScreen =
|
||||
config.ios && config.ios.requireFullScreen;
|
||||
if (infoPlist.UIRequiresFullScreen == null) {
|
||||
// NOTES: This is defaulted to `true` for now to match the behavior prior to SDK 34,
|
||||
// but will change to `false` in a future SDK version.
|
||||
infoPlist.UIRequiresFullScreen = true;
|
||||
}
|
||||
// Cast to make sure that it is a boolean.
|
||||
infoPlist.UIRequiresFullScreen = Boolean(infoPlist.UIRequiresFullScreen);
|
||||
|
||||
// Put `ios.userInterfaceStyle` into `UIUserInterfaceStyle` property of Info.plist
|
||||
const userInterfaceStyle = config.ios && config.ios.userInterfaceStyle;
|
||||
if (userInterfaceStyle) {
|
||||
// To convert our config value to the InfoPlist value, we can just capitalize it.
|
||||
infoPlist.UIUserInterfaceStyle = _mapUserInterfaceStyleForInfoPlist(
|
||||
userInterfaceStyle,
|
||||
);
|
||||
}
|
||||
|
||||
// context-specific plist changes
|
||||
if (context instanceof StandaloneContextUser) {
|
||||
infoPlist = _configureInfoPlistForLocalDevelopment(
|
||||
infoPlist,
|
||||
context.data.exp,
|
||||
);
|
||||
_logDeveloperInfoForLocalDevelopment(infoPlist);
|
||||
}
|
||||
|
||||
if (context instanceof StandaloneContextService && context.build.ios) {
|
||||
infoPlist.CFBundleExecutable = context.build.ios.bundleExecutable;
|
||||
}
|
||||
|
||||
return infoPlist;
|
||||
},
|
||||
);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure EXShell.plist for a standalone app.
|
||||
*/
|
||||
async function _configureShellPlistAsync(
|
||||
context: AnyStandaloneContext,
|
||||
): Promise<void> {
|
||||
const { supportingDirectory } = IosWorkspace.getPaths(context);
|
||||
const config = context.config;
|
||||
const buildPhaseLogger = logger.withFields({
|
||||
buildPhase: 'configuring NSBundle',
|
||||
});
|
||||
|
||||
await IosPlist.modifyAsync(supportingDirectory, 'EXShell', (shellPlist) => {
|
||||
// older SDK versions rely on `isShell` flag in xdl
|
||||
if (parseSdkMajorVersion(config.sdkVersion) < 28) {
|
||||
shellPlist.isShell = true;
|
||||
}
|
||||
shellPlist.manifestUrl = context.published.url;
|
||||
shellPlist.releaseChannel = context.published.releaseChannel;
|
||||
if (
|
||||
isStandaloneContextDataService(context.data) &&
|
||||
context.data.testEnvironment
|
||||
) {
|
||||
shellPlist.testEnvironment = context.data.testEnvironment;
|
||||
}
|
||||
if (config.ios && config.ios.permissions) {
|
||||
shellPlist.permissions = config.ios.permissions;
|
||||
}
|
||||
if (context instanceof StandaloneContextUser) {
|
||||
// disable manifest verification on detached apps until
|
||||
// the developer adds the correct entitlements to their bundle id.
|
||||
shellPlist.isManifestVerificationBypassed = true;
|
||||
}
|
||||
if (config.ios && config.ios.hasOwnProperty('isRemoteJSEnabled')) {
|
||||
// deprecated, overridden by updates.enabled if that exists
|
||||
shellPlist.areRemoteUpdatesEnabled = config.ios.isRemoteJSEnabled;
|
||||
}
|
||||
if (config.updates && config.updates.hasOwnProperty('enabled')) {
|
||||
// enable/disable code push if the developer provided specific behavior
|
||||
shellPlist.areRemoteUpdatesEnabled = config.updates.enabled;
|
||||
}
|
||||
if (config.updates?.hasOwnProperty('checkAutomatically')) {
|
||||
shellPlist.updatesCheckAutomatically =
|
||||
config.updates.checkAutomatically !== 'ON_ERROR_RECOVERY';
|
||||
}
|
||||
if (config.updates?.hasOwnProperty('fallbackToCacheTimeout')) {
|
||||
shellPlist.updatesFallbackToCacheTimeout =
|
||||
config.updates.fallbackToCacheTimeout;
|
||||
}
|
||||
if (
|
||||
!manifestUsesSplashApi(config, 'ios') &&
|
||||
parseSdkMajorVersion(config.sdkVersion) < 28
|
||||
) {
|
||||
// for people still using the old loading api, hide the native splash screen.
|
||||
// we can remove this code eventually.
|
||||
shellPlist.isSplashScreenDisabled = true;
|
||||
}
|
||||
|
||||
buildPhaseLogger.info('Using standalone config:', shellPlist);
|
||||
return shellPlist;
|
||||
});
|
||||
}
|
||||
|
||||
async function _configureConstantsPlistAsync(context: AnyStandaloneContext) {
|
||||
if (context instanceof StandaloneContextUser) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { supportingDirectory } = IosWorkspace.getPaths(context);
|
||||
await IosPlist.modifyAsync(
|
||||
supportingDirectory,
|
||||
'EXBuildConstants',
|
||||
(constantsConfig) => {
|
||||
constantsConfig.API_SERVER_ENDPOINT =
|
||||
process.env.ENVIRONMENT === 'staging'
|
||||
? 'https://staging.exp.host/--/api/v2/'
|
||||
: 'https://exp.host/--/api/v2/';
|
||||
return constantsConfig;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async function _configureGoogleServicesPlistAsync(
|
||||
context: AnyStandaloneContext,
|
||||
): Promise<void> {
|
||||
if (context instanceof StandaloneContextUser) {
|
||||
return;
|
||||
}
|
||||
if (context.data.manifest.ios?.googleServicesFile) {
|
||||
const { supportingDirectory } = IosWorkspace.getPaths(context);
|
||||
await fs.writeFile(
|
||||
path.join(supportingDirectory, 'GoogleService-Info.plist'),
|
||||
context.data.manifest.ios?.googleServicesFile,
|
||||
'base64',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function configureAsync(
|
||||
context: AnyStandaloneContext,
|
||||
): Promise<void> {
|
||||
const buildPhaseLogger = logger.withFields({
|
||||
buildPhase: 'configuring NSBundle',
|
||||
});
|
||||
|
||||
const {
|
||||
intermediatesDirectory,
|
||||
iosProjectDirectory,
|
||||
projectRootDirectory,
|
||||
projectName,
|
||||
supportingDirectory,
|
||||
} = IosWorkspace.getPaths(context);
|
||||
|
||||
const isExpoClientBuild = context.build.isExpoClientBuild();
|
||||
|
||||
if (!isExpoClientBuild && !context.published.url) {
|
||||
throw new Error(`Can't configure a NSBundle without a published url.`);
|
||||
}
|
||||
|
||||
// ensure the intermediates directory is clean of any prior build's artifacts, in the event we
|
||||
// share directories across builds
|
||||
await fs.remove(intermediatesDirectory);
|
||||
|
||||
try {
|
||||
// common configuration for all contexts
|
||||
buildPhaseLogger.info(
|
||||
`Modifying NSBundle configuration at ${supportingDirectory}...`,
|
||||
);
|
||||
await _configureInfoPlistAsync(context);
|
||||
if (!isExpoClientBuild) {
|
||||
await _configureShellPlistAsync(context);
|
||||
}
|
||||
await _configureEntitlementsAsync(context);
|
||||
await _configureConstantsPlistAsync(context);
|
||||
await _configureGoogleServicesPlistAsync(context);
|
||||
if (!isExpoClientBuild) {
|
||||
await IosLaunchScreen.configureLaunchAssetsAsync(
|
||||
context,
|
||||
intermediatesDirectory,
|
||||
);
|
||||
await IosLocalization.writeLocalizationResourcesAsync({
|
||||
projectRoot: projectRootDirectory,
|
||||
supportingDirectory,
|
||||
context,
|
||||
});
|
||||
}
|
||||
|
||||
if (isExpoClientBuild) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (context instanceof StandaloneContextUser) {
|
||||
const iconPath = path.join(
|
||||
iosProjectDirectory,
|
||||
projectName,
|
||||
'Assets.xcassets',
|
||||
'AppIcon.appiconset',
|
||||
);
|
||||
await IosIcons.createAndWriteIconsToPathAsync(context, iconPath);
|
||||
} else if (context instanceof StandaloneContextService) {
|
||||
buildPhaseLogger.info('Bundling assets...');
|
||||
try {
|
||||
await AssetBundle.bundleAsync(
|
||||
context,
|
||||
context.data.manifest.bundledAssets,
|
||||
supportingDirectory,
|
||||
);
|
||||
} catch (e) {
|
||||
throw new Error(`Asset bundling failed: ${e}`);
|
||||
}
|
||||
buildPhaseLogger.info('Compiling resources...');
|
||||
await IosAssetArchive.buildAssetArchiveAsync(
|
||||
context,
|
||||
supportingDirectory,
|
||||
intermediatesDirectory,
|
||||
);
|
||||
await _preloadManifestAndBundleAsync(
|
||||
context.data.manifest,
|
||||
supportingDirectory,
|
||||
getManifestFileNameForSdkVersion(context.data.manifest.sdkVersion),
|
||||
getBundleFileNameForSdkVersion(context.data.manifest.sdkVersion),
|
||||
);
|
||||
}
|
||||
|
||||
await _maybeLegacyPreloadKernelManifestAndBundleAsync(
|
||||
context,
|
||||
'kernel-manifest.json',
|
||||
'kernel.ios.bundle',
|
||||
);
|
||||
} catch (err) {
|
||||
turtleLogger.error(err);
|
||||
throw err;
|
||||
} finally {
|
||||
buildPhaseLogger.info('Cleaning up iOS...');
|
||||
await Promise.all([
|
||||
_cleanPropertyListBackupsAsync(context, supportingDirectory),
|
||||
fs.remove(intermediatesDirectory),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
function _mapUserInterfaceStyleForInfoPlist(
|
||||
userInterfaceStyle: string,
|
||||
): string | undefined {
|
||||
switch (userInterfaceStyle) {
|
||||
case 'light':
|
||||
return 'Light';
|
||||
case 'dark':
|
||||
return 'Dark';
|
||||
case 'automatic':
|
||||
return 'Automatic';
|
||||
default:
|
||||
logger.warn(
|
||||
`User interface style "${userInterfaceStyle}" is not supported. ` +
|
||||
`Supported values: "light", "dark", "automatic".`,
|
||||
);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
import plist from '@expo/plist';
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
|
||||
import { spawnAsyncThrowError } from './ExponentTools';
|
||||
import logger from './Logger';
|
||||
|
||||
function _getNormalizedPlistFilename(plistName: string) {
|
||||
let plistFilename;
|
||||
if (plistName.indexOf('.') !== -1) {
|
||||
plistFilename = plistName;
|
||||
} else {
|
||||
plistFilename = `${plistName}.plist`;
|
||||
}
|
||||
return plistFilename;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param plistName base filename of property list. if no extension, assumes .plist
|
||||
*/
|
||||
async function modifyAsync(plistPath: string, plistName: string, transform: (config: any) => any) {
|
||||
const plistFilename = _getNormalizedPlistFilename(plistName);
|
||||
const configPlistName = path.join(plistPath, plistFilename);
|
||||
const configFilename = path.join(plistPath, `${plistName}.json`);
|
||||
|
||||
// grab original plist as json object
|
||||
let config;
|
||||
if (process.platform === 'darwin') {
|
||||
await spawnAsyncThrowError('plutil', [
|
||||
'-convert',
|
||||
'json',
|
||||
configPlistName,
|
||||
'-o',
|
||||
configFilename,
|
||||
]);
|
||||
const configContents = await fs.readFile(configFilename, 'utf8');
|
||||
|
||||
try {
|
||||
config = JSON.parse(configContents);
|
||||
} catch (e) {
|
||||
logger.info(`Error parsing ${configFilename}`, e);
|
||||
logger.info('The erroneous file contents was:', configContents);
|
||||
config = {};
|
||||
}
|
||||
} else {
|
||||
config = plist.parse(fs.readFileSync(configPlistName, 'utf8'));
|
||||
}
|
||||
|
||||
// apply transformation
|
||||
config = await transform(config);
|
||||
|
||||
// back up old plist and swap in modified one
|
||||
fs.copyFileSync(configPlistName, `${configPlistName}.bak`);
|
||||
await fs.writeFile(configFilename, JSON.stringify(config));
|
||||
if (process.platform === 'darwin') {
|
||||
await spawnAsyncThrowError('plutil', [
|
||||
'-convert',
|
||||
'xml1',
|
||||
configFilename,
|
||||
'-o',
|
||||
configPlistName,
|
||||
]);
|
||||
} else {
|
||||
await fs.writeFile(configPlistName, plist.build(config));
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
async function createBlankAsync(plistPath: string, plistName: string) {
|
||||
// write empty json file
|
||||
const emptyConfig = {};
|
||||
const tmpConfigFile = path.join(plistPath, `${plistName}.json`);
|
||||
await fs.writeFile(tmpConfigFile, JSON.stringify(emptyConfig));
|
||||
|
||||
// convert to plist
|
||||
const plistFilename = _getNormalizedPlistFilename(plistName);
|
||||
const configPlistName = path.join(plistPath, plistFilename);
|
||||
if (process.platform === 'darwin') {
|
||||
await spawnAsyncThrowError('plutil', [
|
||||
'-convert',
|
||||
'xml1',
|
||||
tmpConfigFile,
|
||||
'-o',
|
||||
configPlistName,
|
||||
]);
|
||||
} else {
|
||||
await fs.writeFile(configPlistName, JSON.stringify(plist.build(emptyConfig)));
|
||||
}
|
||||
|
||||
// remove tmp json file
|
||||
fs.removeSync(tmpConfigFile);
|
||||
}
|
||||
|
||||
async function cleanBackupAsync(plistPath: string, plistName: string, restoreOriginal = true) {
|
||||
const plistFilename = _getNormalizedPlistFilename(plistName);
|
||||
const configPlistName = path.join(plistPath, plistFilename);
|
||||
const configFilename = path.join(plistPath, `${plistName}.json`);
|
||||
const backupPlistPath = `${configPlistName}.bak`;
|
||||
|
||||
if (restoreOriginal && (await fs.pathExists(backupPlistPath))) {
|
||||
await fs.copy(backupPlistPath, configPlistName);
|
||||
}
|
||||
|
||||
await fs.remove(backupPlistPath);
|
||||
await fs.remove(configFilename);
|
||||
}
|
||||
|
||||
export { modifyAsync, cleanBackupAsync, createBlankAsync };
|
||||
@@ -0,0 +1,586 @@
|
||||
import JsonFile from '@expo/json-file';
|
||||
import fs from 'fs-extra';
|
||||
import { sync as globSync } from 'glob';
|
||||
import indentString from 'indent-string';
|
||||
import path from 'path';
|
||||
|
||||
import { parseSdkMajorVersion } from './ExponentTools';
|
||||
|
||||
function _validatePodfileSubstitutions(substitutions) {
|
||||
const validKeys = [
|
||||
// a pod dependency on ExpoKit (can be local or remote)
|
||||
'EXPOKIT_DEPENDENCY',
|
||||
// local path to ExpoKit dependency
|
||||
'EXPOKIT_PATH',
|
||||
// tag to use for ExpoKit dependency
|
||||
'EXPOKIT_TAG',
|
||||
// the contents of dependencies.json enumerated as deps in podfile format
|
||||
'EXPONENT_CLIENT_DEPS',
|
||||
// postinstall for detached projects (defines EX_DETACHED among other things)
|
||||
'PODFILE_DETACHED_POSTINSTALL',
|
||||
// same as previous but also defines EX_DETACHED_SERVICE
|
||||
'PODFILE_DETACHED_SERVICE_POSTINSTALL',
|
||||
// ExponentIntegrationTests
|
||||
'PODFILE_TEST_TARGET',
|
||||
// unversioned react native pod dependency, probably at the path given in
|
||||
// REACT_NATIVE_PATH, with a bunch of subspecs.
|
||||
'PODFILE_UNVERSIONED_RN_DEPENDENCY',
|
||||
// postinstall hook for unversioned deps
|
||||
'PODFILE_UNVERSIONED_POSTINSTALL',
|
||||
// versioned rn dependencies (paths to versioned-react-native directories)
|
||||
// read from template files
|
||||
'PODFILE_VERSIONED_RN_DEPENDENCIES',
|
||||
// versioned rn postinstall hooks read from template files
|
||||
'PODFILE_VERSIONED_POSTINSTALLS',
|
||||
// list of generated Expo subspecs to include under a versioned react native dependency
|
||||
'REACT_NATIVE_EXPO_SUBSPECS',
|
||||
// path to use for the unversioned react native dependency
|
||||
'REACT_NATIVE_PATH',
|
||||
// name of the main build target, e.g. Exponent
|
||||
'TARGET_NAME',
|
||||
// path from Podfile to versioned-react-native
|
||||
'VERSIONED_REACT_NATIVE_PATH',
|
||||
// Expo universal modules dependencies
|
||||
'PODFILE_UNVERSIONED_EXPO_MODULES_DEPENDENCIES',
|
||||
// Universal modules configurations to be included in the Podfile
|
||||
'UNIVERSAL_MODULES',
|
||||
// Relative path from iOS project directory to folder where unimodules are installed.
|
||||
'UNIVERSAL_MODULES_PATH',
|
||||
];
|
||||
|
||||
for (const key in substitutions) {
|
||||
if (substitutions.hasOwnProperty(key)) {
|
||||
if (!validKeys.includes(key)) {
|
||||
throw new Error(`Unrecognized Podfile template key: ${key}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function _renderExpoKitDependency(options, sdkVersion) {
|
||||
const sdkMajorVersion = parseSdkMajorVersion(sdkVersion);
|
||||
let attributes;
|
||||
if (options.expoKitPath) {
|
||||
attributes = {
|
||||
path: path.join(options.expoKitPath, 'ios'),
|
||||
};
|
||||
} else if (options.expoKitTag) {
|
||||
attributes = {
|
||||
git: 'http://github.com/expo/expo.git',
|
||||
tag: options.expoKitTag,
|
||||
};
|
||||
} else {
|
||||
attributes = {
|
||||
git: 'http://github.com/expo/expo.git',
|
||||
branch: 'master',
|
||||
};
|
||||
}
|
||||
|
||||
// GL subspec is available as of SDK 26
|
||||
// but removed together with CPP subspec in SDK 29
|
||||
if (sdkMajorVersion < 26) {
|
||||
attributes.subspecs = ['Core', 'CPP'];
|
||||
} else if (sdkMajorVersion < 29 && !process.env.EXPO_UNIVERSE_DIR) {
|
||||
attributes.subspecs = ['Core', 'CPP', 'GL'];
|
||||
} else {
|
||||
attributes.subspecs = ['Core'];
|
||||
}
|
||||
attributes.inhibit_warnings = true;
|
||||
|
||||
const dependency = `pod 'ExpoKit',
|
||||
${indentString(_renderDependencyAttributes(attributes), 2)}`;
|
||||
|
||||
return indentString(dependency, 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param sdkVersion if specified, indicates which sdkVersion this project uses
|
||||
* as 'UNVERSIONED', e.g. if we are detaching a sdk15 project, we render
|
||||
* an unversioned dependency pointing at RN#sdk-15.
|
||||
*/
|
||||
function _renderUnversionedReactNativeDependency(options, sdkVersion) {
|
||||
const sdkMajorVersion = parseSdkMajorVersion(sdkVersion);
|
||||
|
||||
if (sdkMajorVersion >= 39) {
|
||||
return indentString(`
|
||||
# Install React Native and its dependencies
|
||||
require_relative '../node_modules/react-native/scripts/react_native_pods'
|
||||
use_react_native!(production: true)`);
|
||||
}
|
||||
|
||||
if (sdkMajorVersion >= 36) {
|
||||
return indentString(
|
||||
`
|
||||
# Install React Native and its dependencies
|
||||
require_relative '../node_modules/react-native/scripts/autolink-ios.rb'
|
||||
use_react_native!`
|
||||
);
|
||||
}
|
||||
|
||||
const glogLibraryName = sdkMajorVersion < 26 ? 'GLog' : 'glog';
|
||||
return indentString(
|
||||
`
|
||||
${_renderUnversionedReactDependency(options)}
|
||||
${_renderUnversionedYogaDependency(options)}
|
||||
${_renderUnversionedThirdPartyDependency(
|
||||
'DoubleConversion',
|
||||
path.join('third-party-podspecs', 'DoubleConversion.podspec'),
|
||||
options
|
||||
)}
|
||||
${_renderUnversionedThirdPartyDependency(
|
||||
'Folly',
|
||||
path.join('third-party-podspecs', 'Folly.podspec'),
|
||||
options
|
||||
)}
|
||||
${_renderUnversionedThirdPartyDependency(
|
||||
glogLibraryName,
|
||||
path.join('third-party-podspecs', `${glogLibraryName}.podspec`),
|
||||
options
|
||||
)}
|
||||
`,
|
||||
2
|
||||
);
|
||||
}
|
||||
|
||||
function _renderUnversionedReactDependency(options, sdkVersion) {
|
||||
if (!options.reactNativePath) {
|
||||
throw new Error(`Unsupported options for RN dependency: ${options}`);
|
||||
}
|
||||
const attributes = {
|
||||
path: options.reactNativePath,
|
||||
inhibit_warnings: true,
|
||||
subspecs: [
|
||||
'Core',
|
||||
'ART',
|
||||
'RCTActionSheet',
|
||||
'RCTAnimation',
|
||||
'RCTCameraRoll',
|
||||
'RCTGeolocation',
|
||||
'RCTImage',
|
||||
'RCTNetwork',
|
||||
'RCTText',
|
||||
'RCTVibration',
|
||||
'RCTWebSocket',
|
||||
'DevSupport',
|
||||
'CxxBridge',
|
||||
],
|
||||
};
|
||||
return `pod 'React',
|
||||
${indentString(_renderDependencyAttributes(attributes), 2)}`;
|
||||
}
|
||||
|
||||
function _renderUnversionedYogaDependency(options) {
|
||||
let attributes;
|
||||
if (options.reactNativePath) {
|
||||
attributes = {
|
||||
path: path.join(options.reactNativePath, 'ReactCommon', 'yoga'),
|
||||
inhibit_warnings: true,
|
||||
};
|
||||
} else {
|
||||
throw new Error(`Unsupported options for Yoga dependency: ${options}`);
|
||||
}
|
||||
return `pod 'yoga',
|
||||
${indentString(_renderDependencyAttributes(attributes), 2)}`;
|
||||
}
|
||||
|
||||
function _renderUnversionedThirdPartyDependency(podName, podspecRelativePath, options) {
|
||||
let attributes;
|
||||
if (options.reactNativePath) {
|
||||
attributes = {
|
||||
podspec: path.join(options.reactNativePath, podspecRelativePath),
|
||||
inhibit_warnings: true,
|
||||
};
|
||||
} else {
|
||||
throw new Error(`Unsupported options for ${podName} dependency: ${options}`);
|
||||
}
|
||||
return `pod '${podName}',
|
||||
${indentString(_renderDependencyAttributes(attributes), 2)}`;
|
||||
}
|
||||
|
||||
function _renderDependencyAttributes(attributes) {
|
||||
const attributesStrings = [];
|
||||
for (const key of Object.keys(attributes)) {
|
||||
const value = JSON.stringify(attributes[key], null, 2);
|
||||
attributesStrings.push(`:${key} => ${value}`);
|
||||
}
|
||||
return attributesStrings.join(',\n');
|
||||
}
|
||||
|
||||
function createSdkFilterFn(sdkVersion) {
|
||||
if (sdkVersion && String(sdkVersion).toUpperCase() === 'UNVERSIONED') {
|
||||
return () => false;
|
||||
}
|
||||
if (sdkVersion === undefined || !sdkVersion.match(/^\d+\.\d+.\d+$/)) {
|
||||
return;
|
||||
}
|
||||
const sdkVersionWithUnderscores = sdkVersion.replace(/\./g, '_');
|
||||
return i => i.endsWith(`/ReactABI${sdkVersionWithUnderscores}.rb`);
|
||||
}
|
||||
|
||||
async function _renderVersionedReactNativeDependenciesAsync(
|
||||
templatesDirectory,
|
||||
versionedReactNativePath,
|
||||
expoSubspecs,
|
||||
shellAppSdkVersion
|
||||
) {
|
||||
const filterFn = createSdkFilterFn(shellAppSdkVersion);
|
||||
let result = await _concatTemplateFilesInDirectoryAsync(
|
||||
path.join(templatesDirectory, 'versioned-react-native', 'dependencies'),
|
||||
filterFn
|
||||
);
|
||||
expoSubspecs = expoSubspecs.map(subspec => `'${subspec}'`).join(', ');
|
||||
result = result.replace(/\$\{VERSIONED_REACT_NATIVE_PATH\}/g, versionedReactNativePath);
|
||||
result = result.replace(/\$\{REACT_NATIVE_EXPO_SUBSPECS\}/g, expoSubspecs);
|
||||
return result;
|
||||
}
|
||||
|
||||
async function _renderVersionedReactNativePostinstallsAsync(
|
||||
templatesDirectory,
|
||||
shellAppSdkVersion
|
||||
) {
|
||||
const filterFn = createSdkFilterFn(shellAppSdkVersion);
|
||||
return _concatTemplateFilesInDirectoryAsync(
|
||||
path.join(templatesDirectory, 'versioned-react-native', 'postinstalls'),
|
||||
filterFn
|
||||
);
|
||||
}
|
||||
|
||||
async function _concatTemplateFilesInDirectoryAsync(directory, filterFn) {
|
||||
const templateFilenames = globSync('*.rb', { absolute: true, cwd: directory }).sort();
|
||||
const filteredTemplateFilenames = filterFn
|
||||
? templateFilenames.filter(filterFn)
|
||||
: templateFilenames;
|
||||
const templateStrings = [];
|
||||
// perform this in series in order to get deterministic output
|
||||
for (let fileIdx = 0, nFiles = filteredTemplateFilenames.length; fileIdx < nFiles; fileIdx++) {
|
||||
const filename = filteredTemplateFilenames[fileIdx];
|
||||
const templateString = await fs.readFile(filename, 'utf8');
|
||||
if (templateString) {
|
||||
templateStrings.push(templateString);
|
||||
}
|
||||
}
|
||||
return templateStrings.join('\n');
|
||||
}
|
||||
|
||||
function _renderDetachedPostinstall(sdkVersion, isServiceContext) {
|
||||
const sdkMajorVersion = parseSdkMajorVersion(sdkVersion);
|
||||
const podNameExpression = sdkMajorVersion < 33 ? 'target.pod_name' : 'pod_name';
|
||||
const targetExpression = sdkMajorVersion < 33 ? 'target' : 'target_installation_result';
|
||||
|
||||
const podsRootSub = '${PODS_ROOT}';
|
||||
const maybeDetachedServiceDef = isServiceContext
|
||||
? `config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] << 'EX_DETACHED_SERVICE=1'`
|
||||
: '';
|
||||
|
||||
const maybeFrameworkSearchPathDef =
|
||||
sdkMajorVersion < 33
|
||||
? `
|
||||
# Needed for GoogleMaps 2.x
|
||||
config.build_settings['FRAMEWORK_SEARCH_PATHS'] ||= []
|
||||
config.build_settings['FRAMEWORK_SEARCH_PATHS'] << '${podsRootSub}/GoogleMaps/Base/Frameworks'
|
||||
config.build_settings['FRAMEWORK_SEARCH_PATHS'] << '${podsRootSub}/GoogleMaps/Maps/Frameworks'`
|
||||
: '';
|
||||
// In SDK39, in preparation for iOS 14 we've decided to remove IDFA code.
|
||||
// By adding this macro to shell apps we'll remove this code from Branch
|
||||
// on compilation level, see:
|
||||
// https://github.com/BranchMetrics/ios-branch-deep-linking-attribution/blob/ac991f9d0bc9bad640b25a0f1192679a8cfa083a/Branch-SDK/BNCSystemObserver.m#L49-L75
|
||||
const excludeIdfaCodeFromBranchSinceSDK39 =
|
||||
sdkMajorVersion >= 39
|
||||
? `
|
||||
if ${podNameExpression} == 'Branch'
|
||||
${targetExpression}.native_target.build_configurations.each do |config|
|
||||
config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= ['$(inherited)']
|
||||
config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] << 'BRANCH_EXCLUDE_IDFA_CODE=1'
|
||||
end
|
||||
end`
|
||||
: '';
|
||||
return `
|
||||
if ${podNameExpression} == 'ExpoKit'
|
||||
${targetExpression}.native_target.build_configurations.each do |config|
|
||||
config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= ['$(inherited)']
|
||||
config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] << 'EX_DETACHED=1'
|
||||
${maybeDetachedServiceDef}
|
||||
# Enable Google Maps support
|
||||
config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] << 'HAVE_GOOGLE_MAPS=1'
|
||||
config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] << 'HAVE_GOOGLE_MAPS_UTILS=1'
|
||||
${maybeFrameworkSearchPathDef}
|
||||
end
|
||||
end
|
||||
|
||||
${excludeIdfaCodeFromBranchSinceSDK39}
|
||||
`;
|
||||
}
|
||||
|
||||
function _renderUnversionedPostinstall(sdkVersion) {
|
||||
const podsToChangeDeployTarget = [
|
||||
'Amplitude-iOS',
|
||||
'Analytics',
|
||||
'AppAuth',
|
||||
'Branch',
|
||||
'CocoaLumberjack',
|
||||
'FBSDKCoreKit',
|
||||
'FBSDKLoginKit',
|
||||
'FBSDKShareKit',
|
||||
'GPUImage',
|
||||
'JKBigInteger2',
|
||||
];
|
||||
const podsToChangeRB = `[${podsToChangeDeployTarget.map(pod => `'${pod}'`).join(',')}]`;
|
||||
const sdkMajorVersion = parseSdkMajorVersion(sdkVersion);
|
||||
const podNameExpression = sdkMajorVersion < 33 ? 'target.pod_name' : 'pod_name';
|
||||
const targetExpression = sdkMajorVersion < 33 ? 'target' : 'target_installation_result';
|
||||
|
||||
// SDK41 drops support for iOS 10.0
|
||||
const deploymentTarget = sdkMajorVersion > 40 ? '11.0' : '10.0';
|
||||
|
||||
const podsToChangeDeployTargetIfStart =
|
||||
sdkMajorVersion <= 33 ? ` if ${podsToChangeRB}.include? ${podNameExpression}` : '';
|
||||
const podsToChangeDeployTargetIfEnd = sdkMajorVersion <= 33 ? ' end' : '';
|
||||
const gccPreprocessorDefinitionsCondition =
|
||||
sdkMajorVersion < 36
|
||||
? `${podNameExpression} == 'React'`
|
||||
: `${podNameExpression}.start_with?('React')`;
|
||||
|
||||
return `
|
||||
${podsToChangeDeployTargetIfStart}
|
||||
${targetExpression}.native_target.build_configurations.each do |config|
|
||||
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '${deploymentTarget}'
|
||||
end
|
||||
${podsToChangeDeployTargetIfEnd}
|
||||
|
||||
# Can't specify this in the React podspec because we need to use those podspecs for detached
|
||||
# projects which don't reference ExponentCPP.
|
||||
if ${podNameExpression}.start_with?('React')
|
||||
${targetExpression}.native_target.build_configurations.each do |config|
|
||||
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '${deploymentTarget}'
|
||||
config.build_settings['HEADER_SEARCH_PATHS'] ||= ['$(inherited)']
|
||||
end
|
||||
end
|
||||
|
||||
# Build React Native with RCT_DEV enabled and RCT_ENABLE_INSPECTOR and
|
||||
# RCT_ENABLE_PACKAGER_CONNECTION disabled
|
||||
next unless ${gccPreprocessorDefinitionsCondition}
|
||||
${targetExpression}.native_target.build_configurations.each do |config|
|
||||
config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= ['$(inherited)']
|
||||
config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] << 'RCT_DEV=1'
|
||||
config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] << 'RCT_ENABLE_INSPECTOR=0'
|
||||
config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] << 'ENABLE_PACKAGER_CONNECTION=0'
|
||||
end
|
||||
`;
|
||||
}
|
||||
|
||||
function _renderTestTarget(reactNativePath) {
|
||||
return `
|
||||
target 'ExponentIntegrationTests' do
|
||||
inherit! :search_paths
|
||||
end
|
||||
|
||||
target 'Tests' do
|
||||
inherit! :search_paths
|
||||
end
|
||||
`;
|
||||
}
|
||||
|
||||
async function _renderPodDependenciesAsync(dependenciesConfigPath, options) {
|
||||
const dependencies = await new JsonFile(dependenciesConfigPath).readAsync();
|
||||
const type = options.isPodfile ? 'pod' : 'ss.dependency';
|
||||
const noWarningsFlag = options.isPodfile ? `, :inhibit_warnings => true` : '';
|
||||
const depsStrings = dependencies.map(dependency => {
|
||||
let builder = '';
|
||||
if (dependency.comments) {
|
||||
builder += dependency.comments.map(commentLine => ` # ${commentLine}`).join('\n');
|
||||
builder += '\n';
|
||||
}
|
||||
const otherPodfileFlags = options.isPodfile && dependency.otherPodfileFlags;
|
||||
builder += ` ${type} '${dependency.name}', '${dependency.version}'${noWarningsFlag}${
|
||||
otherPodfileFlags || ''
|
||||
}`;
|
||||
return builder;
|
||||
});
|
||||
return depsStrings.join('\n');
|
||||
}
|
||||
|
||||
async function renderExpoKitPodspecAsync(pathToTemplate, pathToOutput, moreSubstitutions) {
|
||||
const templatesDirectory = path.dirname(pathToTemplate);
|
||||
const templateString = await fs.readFile(pathToTemplate, 'utf8');
|
||||
const dependencies = await _renderPodDependenciesAsync(
|
||||
path.join(templatesDirectory, 'dependencies.json'),
|
||||
{ isPodfile: false }
|
||||
);
|
||||
let result = templateString.replace(/\$\{IOS_EXPOKIT_DEPS\}/g, indentString(dependencies, 2));
|
||||
if (moreSubstitutions && moreSubstitutions.IOS_EXPONENT_CLIENT_VERSION) {
|
||||
result = result.replace(
|
||||
/\$\{IOS_EXPONENT_CLIENT_VERSION\}/g,
|
||||
moreSubstitutions.IOS_EXPONENT_CLIENT_VERSION
|
||||
);
|
||||
}
|
||||
|
||||
await fs.writeFile(pathToOutput, result);
|
||||
}
|
||||
|
||||
function _renderUnversionedUniversalModulesDependencies(
|
||||
universalModules,
|
||||
universalModulesPath,
|
||||
sdkVersion
|
||||
) {
|
||||
const sdkMajorVersion = parseSdkMajorVersion(sdkVersion);
|
||||
|
||||
if (sdkMajorVersion >= 33) {
|
||||
const excludedUnimodules = [
|
||||
'expo-bluetooth',
|
||||
'expo-in-app-purchases',
|
||||
'expo-payments-stripe',
|
||||
'expo-module-template',
|
||||
'expo-image',
|
||||
];
|
||||
|
||||
if (sdkMajorVersion < 39) {
|
||||
excludedUnimodules.push('expo-splash-screen', 'expo-updates');
|
||||
}
|
||||
|
||||
const expoModulesThatAreNotUnimodules = [];
|
||||
if (sdkMajorVersion >= 39) {
|
||||
expoModulesThatAreNotUnimodules.push(
|
||||
`pod 'EXRandom', path: '${universalModulesPath}/expo-random/ios'`
|
||||
);
|
||||
}
|
||||
|
||||
return indentString(
|
||||
`
|
||||
# Install unimodules
|
||||
require_relative '../node_modules/react-native-unimodules/cocoapods.rb'
|
||||
use_unimodules!(
|
||||
modules_paths: ['${universalModulesPath}'],
|
||||
exclude: [
|
||||
${excludedUnimodules.map(module => `'${module}'`).join(',\n ')}
|
||||
],
|
||||
)
|
||||
|
||||
# Expo modules that are not unimodules
|
||||
${expoModulesThatAreNotUnimodules.join('\n')}
|
||||
`,
|
||||
2
|
||||
);
|
||||
} else {
|
||||
return indentString(
|
||||
universalModules
|
||||
.map(moduleInfo =>
|
||||
_renderUnversionedUniversalModuleDependency(
|
||||
moduleInfo.podName,
|
||||
moduleInfo.path,
|
||||
sdkVersion
|
||||
)
|
||||
)
|
||||
.join('\n'),
|
||||
2
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function _renderUnversionedUniversalModuleDependency(podName, path, sdkVersion) {
|
||||
const attributes = {
|
||||
path,
|
||||
};
|
||||
return `pod '${podName}',
|
||||
${indentString(_renderDependencyAttributes(attributes), 2)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param pathToTemplate path to template Podfile
|
||||
* @param pathToOutput path to render final Podfile
|
||||
* @param moreSubstitutions dictionary of additional substitution keys and values to replace
|
||||
* in the template, such as: TARGET_NAME, REACT_NATIVE_PATH
|
||||
*/
|
||||
async function renderPodfileAsync(
|
||||
pathToTemplate,
|
||||
pathToOutput,
|
||||
moreSubstitutions,
|
||||
shellAppSdkVersion,
|
||||
sdkVersion = 'UNVERSIONED'
|
||||
) {
|
||||
if (!moreSubstitutions) {
|
||||
moreSubstitutions = {};
|
||||
}
|
||||
const templatesDirectory = path.dirname(pathToTemplate);
|
||||
const templateString = await fs.readFile(pathToTemplate, 'utf8');
|
||||
|
||||
const reactNativePath = moreSubstitutions.REACT_NATIVE_PATH;
|
||||
let rnDependencyOptions;
|
||||
if (reactNativePath) {
|
||||
rnDependencyOptions = { reactNativePath };
|
||||
} else {
|
||||
rnDependencyOptions = {};
|
||||
}
|
||||
|
||||
const expoKitPath = moreSubstitutions.EXPOKIT_PATH;
|
||||
const expoKitTag = moreSubstitutions.EXPOKIT_TAG;
|
||||
let expoKitDependencyOptions = {};
|
||||
if (expoKitPath) {
|
||||
expoKitDependencyOptions = { expoKitPath };
|
||||
} else if (expoKitTag) {
|
||||
expoKitDependencyOptions = { expoKitTag };
|
||||
}
|
||||
|
||||
let versionedRnPath = moreSubstitutions.VERSIONED_REACT_NATIVE_PATH;
|
||||
if (!versionedRnPath) {
|
||||
versionedRnPath = './versioned-react-native';
|
||||
}
|
||||
let rnExpoSubspecs = moreSubstitutions.REACT_NATIVE_EXPO_SUBSPECS;
|
||||
if (!rnExpoSubspecs) {
|
||||
rnExpoSubspecs = ['Expo'];
|
||||
}
|
||||
|
||||
const versionedDependencies = await _renderVersionedReactNativeDependenciesAsync(
|
||||
templatesDirectory,
|
||||
versionedRnPath,
|
||||
rnExpoSubspecs,
|
||||
shellAppSdkVersion
|
||||
);
|
||||
const versionedPostinstalls = await _renderVersionedReactNativePostinstallsAsync(
|
||||
templatesDirectory,
|
||||
shellAppSdkVersion
|
||||
);
|
||||
const podDependencies = await _renderPodDependenciesAsync(
|
||||
path.join(templatesDirectory, 'dependencies.json'),
|
||||
{ isPodfile: true }
|
||||
);
|
||||
|
||||
let universalModules = moreSubstitutions.UNIVERSAL_MODULES;
|
||||
if (!universalModules) {
|
||||
universalModules = [];
|
||||
}
|
||||
|
||||
const substitutions = {
|
||||
EXPONENT_CLIENT_DEPS: podDependencies,
|
||||
EXPOKIT_DEPENDENCY: _renderExpoKitDependency(expoKitDependencyOptions, sdkVersion),
|
||||
PODFILE_UNVERSIONED_EXPO_MODULES_DEPENDENCIES: _renderUnversionedUniversalModulesDependencies(
|
||||
universalModules,
|
||||
moreSubstitutions.UNIVERSAL_MODULES_PATH,
|
||||
sdkVersion
|
||||
),
|
||||
PODFILE_UNVERSIONED_RN_DEPENDENCY: _renderUnversionedReactNativeDependency(
|
||||
rnDependencyOptions,
|
||||
sdkVersion
|
||||
),
|
||||
PODFILE_UNVERSIONED_POSTINSTALL: _renderUnversionedPostinstall(sdkVersion),
|
||||
PODFILE_DETACHED_POSTINSTALL: _renderDetachedPostinstall(sdkVersion, false),
|
||||
PODFILE_DETACHED_SERVICE_POSTINSTALL: _renderDetachedPostinstall(sdkVersion, true),
|
||||
PODFILE_VERSIONED_RN_DEPENDENCIES: versionedDependencies,
|
||||
PODFILE_VERSIONED_POSTINSTALLS: versionedPostinstalls,
|
||||
PODFILE_TEST_TARGET: shellAppSdkVersion ? '' : _renderTestTarget(reactNativePath),
|
||||
...moreSubstitutions,
|
||||
};
|
||||
_validatePodfileSubstitutions(substitutions);
|
||||
|
||||
let result = templateString;
|
||||
for (const key in substitutions) {
|
||||
if (substitutions.hasOwnProperty(key)) {
|
||||
const replacement = substitutions[key];
|
||||
result = result.replace(new RegExp(`\\$\\{${key}\\}`, 'g'), replacement);
|
||||
}
|
||||
}
|
||||
|
||||
await fs.writeFile(pathToOutput, result);
|
||||
}
|
||||
|
||||
export { renderExpoKitPodspecAsync, renderPodfileAsync };
|
||||
@@ -0,0 +1,89 @@
|
||||
import plist, { PlistObject } from '@expo/plist';
|
||||
import fs from 'fs';
|
||||
import { sync as globSync } from 'glob';
|
||||
import xcode from 'xcode';
|
||||
|
||||
const defaultBundleId = '$(PRODUCT_BUNDLE_IDENTIFIER)';
|
||||
|
||||
export function resetAllPlistBundleIdentifiers(projectRoot: string) {
|
||||
const infoPlistPaths = globSync('ios/*/Info.plist', { absolute: true, cwd: projectRoot });
|
||||
|
||||
for (const plistPath of infoPlistPaths) {
|
||||
resetPlistBundleIdentifier(plistPath);
|
||||
}
|
||||
}
|
||||
|
||||
export function resetPlistBundleIdentifier(plistPath: string) {
|
||||
// Read Plist as source
|
||||
const rawPlist = fs.readFileSync(plistPath, 'utf8');
|
||||
|
||||
const plistObject = plist.parse(rawPlist) as PlistObject;
|
||||
|
||||
if (plistObject.CFBundleIdentifier) {
|
||||
// Maybe bail out
|
||||
if (plistObject.CFBundleIdentifier === defaultBundleId) { return; }
|
||||
// attempt to match default Info.plist format
|
||||
const format = { pretty: true, indent: `\t` };
|
||||
|
||||
const xml = plist.build(
|
||||
{
|
||||
...plistObject,
|
||||
CFBundleIdentifier: defaultBundleId,
|
||||
},
|
||||
format,
|
||||
);
|
||||
|
||||
if (xml !== rawPlist) {
|
||||
fs.writeFileSync(plistPath, xml);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function filterComments([item]: any[]): boolean {
|
||||
return !item.endsWith(`_comment`);
|
||||
}
|
||||
function filterConfig(input: any[]): boolean {
|
||||
const {
|
||||
1: { isa },
|
||||
} = input;
|
||||
return isa === 'XCBuildConfiguration';
|
||||
}
|
||||
function filterHosts(input: any[]): boolean {
|
||||
const {
|
||||
1: { buildSettings },
|
||||
} = input;
|
||||
|
||||
return !buildSettings.TEST_HOST;
|
||||
}
|
||||
|
||||
export function updateBundleIdentifierForPbxproj(pbxprojPath: string, bundleIdentifier: string) {
|
||||
const project = xcode.project(pbxprojPath);
|
||||
project.parseSync();
|
||||
|
||||
Object.entries(project.pbxXCBuildConfigurationSection())
|
||||
.filter(filterComments)
|
||||
.filter(filterConfig)
|
||||
.filter(filterHosts)
|
||||
.forEach(({ 1: { buildSettings } }: any) => {
|
||||
if (buildSettings.PRODUCT_BUNDLE_IDENTIFIER === bundleIdentifier) {
|
||||
return;
|
||||
}
|
||||
|
||||
buildSettings.PRODUCT_BUNDLE_IDENTIFIER = `"${bundleIdentifier}"`;
|
||||
|
||||
const productName = bundleIdentifier.split('.').pop();
|
||||
if (!productName?.includes('$')) {
|
||||
buildSettings.PRODUCT_NAME = productName;
|
||||
}
|
||||
});
|
||||
fs.writeFileSync(pbxprojPath, project.writeSync());
|
||||
}
|
||||
|
||||
export function setBundleIdentifier(projectRoot: string, bundleIdentifier: string) {
|
||||
// Get all pbx projects
|
||||
const pbxprojPaths = globSync('ios/*/project.pbxproj', { absolute: true, cwd: projectRoot });
|
||||
|
||||
for (const pbxprojPath of pbxprojPaths) {
|
||||
updateBundleIdentifierForPbxproj(pbxprojPath, bundleIdentifier);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,371 @@
|
||||
import fs from 'fs-extra';
|
||||
import pascalCase from 'pascal-case';
|
||||
import path from 'path';
|
||||
import rimraf from 'rimraf';
|
||||
|
||||
import {
|
||||
getManifestAsync,
|
||||
parseSdkMajorVersion,
|
||||
spawnAsync,
|
||||
spawnAsyncThrowError,
|
||||
} from './ExponentTools';
|
||||
import * as IosNSBundle from './IosNSBundle';
|
||||
import * as IosWorkspace from './IosWorkspace';
|
||||
import logger from './Logger';
|
||||
import StandaloneBuildFlags from './StandaloneBuildFlags';
|
||||
import StandaloneContext from './StandaloneContext';
|
||||
|
||||
export const EXPOKIT_APP = 'ExpoKitApp';
|
||||
export const EXPONENT_APP = 'Exponent';
|
||||
|
||||
function _validateCLIArgs(args) {
|
||||
args.type = args.type || 'archive';
|
||||
args.configuration = args.configuration || 'Release';
|
||||
args.verbose = args.verbose || false;
|
||||
args.testEnvironment = args.testEnvironment || 'none';
|
||||
|
||||
switch (args.type) {
|
||||
case 'simulator': {
|
||||
if (args.configuration !== 'Debug' && args.configuration !== 'Release') {
|
||||
throw new Error(`Unsupported build configuration ${args.configuration}`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'archive': {
|
||||
if (args.configuration !== 'Release') {
|
||||
throw new Error('Release is the only supported configuration when archiving');
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'client':
|
||||
break;
|
||||
default: {
|
||||
throw new Error(`Unsupported build type ${args.type}`);
|
||||
}
|
||||
}
|
||||
|
||||
switch (args.action) {
|
||||
case 'configure': {
|
||||
if (args.type === 'client') {
|
||||
break;
|
||||
}
|
||||
|
||||
if (!args.url) {
|
||||
throw new Error('Must run with `--url MANIFEST_URL`');
|
||||
}
|
||||
if (!args.sdkVersion) {
|
||||
throw new Error('Must run with `--sdkVersion SDK_VERSION`');
|
||||
}
|
||||
if (!args.archivePath) {
|
||||
throw new Error(
|
||||
'Need to provide --archivePath <path to existing archive for configuration>'
|
||||
);
|
||||
}
|
||||
if (
|
||||
args.testEnvironment !== 'local' &&
|
||||
args.testEnvironment !== 'ci' &&
|
||||
args.testEnvironment !== 'none'
|
||||
) {
|
||||
throw new Error(`Unsupported test environment ${args.testEnvironment}`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'build': {
|
||||
break;
|
||||
}
|
||||
case 'create-workspace': {
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
throw new Error(`Unsupported build action ${args.action}`);
|
||||
}
|
||||
}
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the iOS workspace at the given path.
|
||||
* @return the path to the resulting build artifact
|
||||
*/
|
||||
async function _buildAsync(
|
||||
projectName,
|
||||
workspacePath,
|
||||
configuration,
|
||||
type,
|
||||
relativeBuildDestination,
|
||||
verbose,
|
||||
useModernBuildSystem = false
|
||||
) {
|
||||
const modernBuildSystemFragment = `-UseModernBuildSystem=${useModernBuildSystem ? 'YES' : 'NO'}`;
|
||||
const buildDest = `${relativeBuildDestination}-${type}`;
|
||||
let buildCmd = `set -o pipefail && xcodebuild -workspace ${projectName}.xcworkspace -scheme ${projectName} -configuration ${configuration} -derivedDataPath ${buildDest} ${modernBuildSystemFragment}`,
|
||||
pathToArtifact;
|
||||
if (type === 'simulator') {
|
||||
buildCmd += ` -sdk iphonesimulator CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO ARCHS="x86_64" ONLY_ACTIVE_ARCH=NO | xcpretty`;
|
||||
pathToArtifact = path.join(
|
||||
buildDest,
|
||||
'Build',
|
||||
'Products',
|
||||
`${configuration}-iphonesimulator`,
|
||||
`${projectName}.app`
|
||||
);
|
||||
} else if (type === 'archive') {
|
||||
buildCmd += ` -sdk iphoneos -destination generic/platform=iOS archive -archivePath ${buildDest}/${projectName}.xcarchive CODE_SIGNING_ALLOWED=NO | xcpretty`;
|
||||
pathToArtifact = path.join(buildDest, `${projectName}.xcarchive`);
|
||||
} else {
|
||||
throw new Error(`Unsupported build type: ${type}`);
|
||||
}
|
||||
|
||||
logger.info(`Building iOS workspace at ${workspacePath} to ${buildDest}:\n`);
|
||||
logger.info(buildCmd);
|
||||
if (!verbose) {
|
||||
logger.info(
|
||||
'\nxcodebuild is running. Logging errors only. To see full output, use --verbose 1...'
|
||||
);
|
||||
}
|
||||
await spawnAsyncThrowError(buildCmd, null, {
|
||||
// only stderr
|
||||
stdio: verbose ? 'inherit' : ['ignore', 'ignore', 'inherit'],
|
||||
cwd: workspacePath,
|
||||
shell: true,
|
||||
});
|
||||
return path.resolve(workspacePath, pathToArtifact);
|
||||
}
|
||||
|
||||
async function _podInstallAsync(workspacePath, isRepoUpdateEnabled) {
|
||||
// ensure pods are clean
|
||||
const pathsToClean = [path.join(workspacePath, 'Pods'), path.join(workspacePath, 'Podfile.lock')];
|
||||
pathsToClean.forEach(path => {
|
||||
if (fs.existsSync(path)) {
|
||||
rimraf.sync(path);
|
||||
}
|
||||
});
|
||||
|
||||
// Disable cocoapod stats to speed up the install
|
||||
const COCOAPODS_DISABLE_STATS = process.env.COCOAPODS_DISABLE_STATS;
|
||||
process.env.COCOAPODS_DISABLE_STATS = true;
|
||||
|
||||
// install
|
||||
const cocoapodsArgs = ['install'];
|
||||
if (isRepoUpdateEnabled) {
|
||||
cocoapodsArgs.push('--repo-update');
|
||||
}
|
||||
logger.info('Installing iOS workspace dependencies...');
|
||||
logger.info(`pod ${cocoapodsArgs.join(' ')}`);
|
||||
try {
|
||||
await spawnAsyncThrowError('pod', cocoapodsArgs, {
|
||||
stdio: 'inherit',
|
||||
cwd: workspacePath,
|
||||
});
|
||||
} finally {
|
||||
// Revert the stats to the user preference
|
||||
process.env.COCOAPODS_DISABLE_STATS = COCOAPODS_DISABLE_STATS;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param workspacePath optionally provide a path for the unbuilt xcode workspace to create/use.
|
||||
* @param expoSourcePath path to expo client app sourcecode (/ios dir from expo/expo repo)
|
||||
* @param shellAppSdkVersion sdk version for shell app
|
||||
*/
|
||||
async function _createStandaloneContextAsync(args) {
|
||||
// right now we only ever build a single detached workspace for service contexts.
|
||||
// TODO: support multiple different pod configurations, assemble a cache of those builds.
|
||||
const expoSourcePath = args.expoSourcePath || '../ios';
|
||||
let workspaceSourcePath;
|
||||
if (args.workspacePath) {
|
||||
workspaceSourcePath = args.workspacePath;
|
||||
} else {
|
||||
workspaceSourcePath = path.join(expoSourcePath, '..', 'shellAppWorkspaces', 'default', 'ios');
|
||||
}
|
||||
const { privateConfigFile, privateConfigData } = args;
|
||||
|
||||
let privateConfig;
|
||||
if (privateConfigData) {
|
||||
privateConfig = privateConfigData;
|
||||
} else if (privateConfigFile) {
|
||||
const privateConfigContents = await fs.readFile(privateConfigFile, 'utf8');
|
||||
privateConfig = JSON.parse(privateConfigContents);
|
||||
}
|
||||
|
||||
let manifest;
|
||||
if (args.manifest) {
|
||||
manifest = args.manifest;
|
||||
logger
|
||||
.withFields({ buildPhase: 'reading manifest' })
|
||||
.info('Using manifest:', JSON.stringify(manifest));
|
||||
} else if (args.url && args.sdkVersion) {
|
||||
const { url, sdkVersion, releaseChannel } = args;
|
||||
manifest = await getManifestAsync(url, {
|
||||
'Exponent-SDK-Version': sdkVersion,
|
||||
'Exponent-Platform': 'ios',
|
||||
'Expo-Release-Channel': releaseChannel ? releaseChannel : 'default',
|
||||
Accept: 'application/expo+json,application/json',
|
||||
});
|
||||
}
|
||||
|
||||
let bundleExecutable = args.type === 'client' ? EXPONENT_APP : EXPOKIT_APP;
|
||||
if (manifest?.ios?.infoPlist?.CFBundleExecutable) {
|
||||
bundleExecutable = manifest.ios.infoPlist.CFBundleExecutable;
|
||||
} else if (privateConfig?.bundleIdentifier) {
|
||||
bundleExecutable = pascalCase(privateConfig.bundleIdentifier);
|
||||
}
|
||||
|
||||
const buildFlags = StandaloneBuildFlags.createIos(args.configuration, {
|
||||
workspaceSourcePath,
|
||||
appleTeamId: args.appleTeamId,
|
||||
buildType: args.type,
|
||||
bundleExecutable,
|
||||
});
|
||||
const context = StandaloneContext.createServiceContext(
|
||||
expoSourcePath,
|
||||
args.archivePath,
|
||||
manifest,
|
||||
privateConfig,
|
||||
args.testEnvironment,
|
||||
buildFlags,
|
||||
args.url,
|
||||
args.releaseChannel,
|
||||
args.shellAppSdkVersion
|
||||
);
|
||||
return context;
|
||||
}
|
||||
|
||||
/**
|
||||
* possible args:
|
||||
* @param url manifest url for shell experience
|
||||
* @param sdkVersion sdk to use when requesting the manifest
|
||||
* @param releaseChannel channel to pull manifests from, default is 'default'
|
||||
* @param archivePath path to existing NSBundle to configure
|
||||
* @param privateConfigFile path to a private config file containing, e.g., private api keys
|
||||
* @param appleTeamId Apple Developer's account Team ID
|
||||
* @param output specify the output path of the configured archive (ie) /tmp/my-app-archive-build.xcarchive or /tmp/my-app-ios-build.tar.gz
|
||||
* @param type type of artifact to configure (simulator or archive)
|
||||
* @param expoSourcePath path to expo client app sourcecode (/ios dir from expo/expo repo)
|
||||
*/
|
||||
async function configureAndCopyArchiveAsync(args) {
|
||||
args = _validateCLIArgs(args);
|
||||
const { output, type } = args;
|
||||
const context = await _createStandaloneContextAsync(args);
|
||||
await IosNSBundle.configureAsync(context);
|
||||
if (output) {
|
||||
const workspaceName = type === 'client' ? EXPONENT_APP : EXPOKIT_APP;
|
||||
if (context.build.ios.bundleExecutable !== workspaceName) {
|
||||
await spawnAsync('/bin/mv', [workspaceName, context.build.ios.bundleExecutable], {
|
||||
pipeToLogger: true,
|
||||
cwd: context.data.archivePath,
|
||||
loggerFields: { buildPhase: 'renaming bundle executable' },
|
||||
});
|
||||
}
|
||||
if (type === 'simulator') {
|
||||
const archiveName = context.config.slug.replace(/[^0-9a-z_-]/gi, '_');
|
||||
const appReleasePath = path.resolve(context.data.archivePath, '..');
|
||||
await spawnAsync(
|
||||
`mv ${workspaceName}.app ${archiveName}.app && tar -czvf ${output} ${archiveName}.app`,
|
||||
null,
|
||||
{
|
||||
stdoutOnly: true,
|
||||
pipeToLogger: true,
|
||||
loggerFields: { buildPhase: 'creating an archive for simulator' },
|
||||
cwd: appReleasePath,
|
||||
shell: true,
|
||||
}
|
||||
);
|
||||
} else if (type === 'archive' || type === 'client') {
|
||||
await spawnAsync('/bin/mv', [`${workspaceName}.xcarchive`, output], {
|
||||
pipeToLogger: true,
|
||||
cwd: path.join(context.data.archivePath, '../../../..'),
|
||||
loggerFields: { buildPhase: 'renaming archive' },
|
||||
});
|
||||
}
|
||||
}
|
||||
return context.data.manifest;
|
||||
}
|
||||
|
||||
/**
|
||||
* possible args:
|
||||
* @param skipRepoUpdate if true, omit `--repo-update` cocoapods flag.
|
||||
*/
|
||||
async function _createTurtleWorkspaceAsync(context, args) {
|
||||
const { skipRepoUpdate } = args;
|
||||
if (fs.existsSync(context.build.ios.workspaceSourcePath)) {
|
||||
logger.info(`Removing existing workspace at ${context.build.ios.workspaceSourcePath}...`);
|
||||
try {
|
||||
rimraf.sync(context.build.ios.workspaceSourcePath);
|
||||
} catch {}
|
||||
}
|
||||
await IosWorkspace.createDetachedAsync(context);
|
||||
await _podInstallAsync(context.build.ios.workspaceSourcePath, !skipRepoUpdate);
|
||||
}
|
||||
|
||||
/**
|
||||
* External-facing version can be used to create a turtle workspace without building it.
|
||||
* Probably only useful for local testing.
|
||||
*
|
||||
* @param workspacePath (optional) provide some other path to create the workspace besides the default
|
||||
* @param url (optional, with sdkVersion) url to an expo manifest, if you want the workspace to be configured automatically
|
||||
* @param sdkVersion (optional, with url) sdkVersion to an expo manifest, if you want the workspace to be configured automatically
|
||||
*/
|
||||
async function createTurtleWorkspaceAsync(args) {
|
||||
args = _validateCLIArgs(args);
|
||||
if (!args.workspacePath) {
|
||||
logger.info(
|
||||
'No workspace path was provided with --workspacePath, so the default will be used.'
|
||||
);
|
||||
}
|
||||
const context = await _createStandaloneContextAsync(args);
|
||||
await _createTurtleWorkspaceAsync(context, args);
|
||||
logger.info(
|
||||
`Created turtle workspace at ${context.build.ios.workspaceSourcePath}. You can open and run this in Xcode.`
|
||||
);
|
||||
if (context.config) {
|
||||
await IosNSBundle.configureAsync(context);
|
||||
logger.info(
|
||||
`The turtle workspace was configured for the url ${args.url}. To run this app with a Debug scheme, make sure to add a development url to 'EXBuildConstants.plist'.`
|
||||
);
|
||||
} else {
|
||||
logger.info(
|
||||
`You can specify --url <manifestUrl> --sdkVersion <version> to configure this workspace as a particular Expo app.\n\nBecause those arguments were omitted, the workspace has not been configured. It will compile but not run. The minimum configuration to get something running is to specify a manifest url in 'EXShell.plist' (for Release builds) or 'EXBuildConstants.plist' (for Debug builds).`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* possible args:
|
||||
* @param configuration StandaloneBuildConfiguration (Debug or Release)
|
||||
* @param verbose show all xcodebuild output (default false)
|
||||
* @param reuseWorkspace if true, when building, assume a detached workspace already exists rather than creating a new one.
|
||||
* @param type type of artifact to build (simulator or archive)
|
||||
* @param shellAppSdkVersion sdk version for shell app
|
||||
*/
|
||||
async function buildAndCopyArtifactAsync(args) {
|
||||
args = _validateCLIArgs(args);
|
||||
const context = await _createStandaloneContextAsync(args);
|
||||
const { verbose, type, reuseWorkspace } = args;
|
||||
const { projectName } = IosWorkspace.getPaths(context);
|
||||
|
||||
if (!reuseWorkspace) {
|
||||
await _createTurtleWorkspaceAsync(context, args);
|
||||
}
|
||||
const pathToArtifact = await _buildAsync(
|
||||
projectName,
|
||||
context.build.ios.workspaceSourcePath,
|
||||
context.build.configuration,
|
||||
type,
|
||||
path.relative(context.build.ios.workspaceSourcePath, './shellAppBase'),
|
||||
verbose,
|
||||
parseSdkMajorVersion(args.shellAppSdkVersion) > 33
|
||||
);
|
||||
const artifactDestPath = path.join('./shellAppBase-builds', type, context.build.configuration);
|
||||
logger.info(`\nFinished building, copying artifact to ${path.resolve(artifactDestPath)}...`);
|
||||
if (fs.existsSync(artifactDestPath)) {
|
||||
await spawnAsyncThrowError('/bin/rm', ['-rf', artifactDestPath]);
|
||||
}
|
||||
logger.info(`mkdir -p ${artifactDestPath}`);
|
||||
await spawnAsyncThrowError('/bin/mkdir', ['-p', artifactDestPath]);
|
||||
logger.info(`cp -R ${pathToArtifact} ${artifactDestPath}`);
|
||||
await spawnAsyncThrowError('/bin/cp', ['-R', pathToArtifact, artifactDestPath]);
|
||||
}
|
||||
|
||||
export { buildAndCopyArtifactAsync, configureAndCopyArchiveAsync, createTurtleWorkspaceAsync };
|
||||
@@ -0,0 +1,448 @@
|
||||
import { Api, Versions } from '@expo/xdl';
|
||||
import assert from 'assert';
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
import rimraf from 'rimraf';
|
||||
|
||||
import * as Modules from '../modules/Modules';
|
||||
import {
|
||||
isDirectory,
|
||||
parseSdkMajorVersion,
|
||||
rimrafDontThrow,
|
||||
spawnAsyncThrowError,
|
||||
transformFileContentsAsync,
|
||||
} from './ExponentTools';
|
||||
import installPackagesAsync from './installPackagesAsync';
|
||||
import * as IosPlist from './IosPlist';
|
||||
import { renderPodfileAsync } from './IosPodsTools.js';
|
||||
import logger from './Logger';
|
||||
import * as Utils from './Utils';
|
||||
|
||||
export { setBundleIdentifier } from './IosSetBundleIdentifier';
|
||||
|
||||
async function _getVersionedExpoKitConfigAsync(sdkVersion, skipServerValidation) {
|
||||
const versions = await Versions.versionsAsync();
|
||||
let sdkVersionConfig = versions.sdkVersions[sdkVersion];
|
||||
if (!sdkVersionConfig) {
|
||||
if (skipServerValidation) {
|
||||
sdkVersionConfig = {};
|
||||
} else {
|
||||
throw new Error(`Unsupported SDK version: ${sdkVersion}`);
|
||||
}
|
||||
}
|
||||
const { iosVersion, iosExpoViewUrl } = sdkVersionConfig;
|
||||
const iosClientVersion = iosVersion ? iosVersion : versions.iosVersion;
|
||||
return {
|
||||
iosClientVersion,
|
||||
iosExpoViewUrl,
|
||||
};
|
||||
}
|
||||
|
||||
async function _getOrCreateTemplateDirectoryAsync(context, iosExpoViewUrl) {
|
||||
if (context.type === 'service') {
|
||||
return path.join(context.data.expoSourcePath, '..');
|
||||
} else if (context.type === 'user') {
|
||||
let expoRootTemplateDirectory;
|
||||
if (process.env.EXPO_VIEW_DIR) {
|
||||
// Only for testing
|
||||
expoRootTemplateDirectory = process.env.EXPO_VIEW_DIR;
|
||||
} else {
|
||||
// HEY: if you need other paths into the extracted archive, be sure and include them
|
||||
// when the archive is generated in `ios/pipeline.js`
|
||||
expoRootTemplateDirectory = path.join(context.data.projectPath, 'temp-ios-directory');
|
||||
if (!isDirectory(expoRootTemplateDirectory)) {
|
||||
fs.mkdirpSync(expoRootTemplateDirectory);
|
||||
logger.info('Downloading iOS code...');
|
||||
assert(iosExpoViewUrl, `The URL for ExpoKit iOS must be set`);
|
||||
await Api.downloadAsync(iosExpoViewUrl, expoRootTemplateDirectory, {
|
||||
extract: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
return expoRootTemplateDirectory;
|
||||
}
|
||||
}
|
||||
|
||||
async function _renameAndMoveProjectFilesAsync(context, projectDirectory, projectName) {
|
||||
// remove .gitignore, as this actually pertains to internal expo template management
|
||||
try {
|
||||
const gitIgnorePath = path.join(projectDirectory, '.gitignore');
|
||||
if (fs.existsSync(gitIgnorePath)) {
|
||||
rimraf.sync(gitIgnorePath);
|
||||
}
|
||||
} catch (e) {}
|
||||
|
||||
const filesToTransform = [
|
||||
path.join('exponent-view-template.xcodeproj', 'project.pbxproj'),
|
||||
path.join('exponent-view-template.xcworkspace', 'contents.xcworkspacedata'),
|
||||
path.join(
|
||||
'exponent-view-template.xcodeproj',
|
||||
'xcshareddata',
|
||||
'xcschemes',
|
||||
'exponent-view-template.xcscheme'
|
||||
),
|
||||
];
|
||||
|
||||
let bundleIdentifier;
|
||||
if (context.type === 'user') {
|
||||
const exp = context.data.exp;
|
||||
bundleIdentifier = exp.ios && exp.ios.bundleIdentifier ? exp.ios.bundleIdentifier : null;
|
||||
if (!bundleIdentifier) {
|
||||
throw new Error(`Cannot configure an ExpoKit workspace with no iOS bundle identifier.`);
|
||||
}
|
||||
} else if (context.type === 'service') {
|
||||
bundleIdentifier = 'host.exp.Exponent';
|
||||
}
|
||||
|
||||
await Promise.all(
|
||||
filesToTransform.map(fileName =>
|
||||
transformFileContentsAsync(path.join(projectDirectory, fileName), fileString => {
|
||||
return fileString
|
||||
.replace(/com.getexponent.exponent-view-template/g, bundleIdentifier)
|
||||
.replace(/exponent-view-template/g, projectName);
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
// order of this array matters
|
||||
const filesToMove = [
|
||||
'exponent-view-template',
|
||||
path.join(
|
||||
'exponent-view-template.xcodeproj',
|
||||
'xcshareddata',
|
||||
'xcschemes',
|
||||
'exponent-view-template.xcscheme'
|
||||
),
|
||||
'exponent-view-template.xcodeproj',
|
||||
'exponent-view-template.xcworkspace',
|
||||
];
|
||||
|
||||
filesToMove.forEach(async fileName => {
|
||||
const destFileName = path.join(
|
||||
path.dirname(fileName),
|
||||
`${projectName}${path.extname(fileName)}`
|
||||
);
|
||||
await spawnAsyncThrowError('/bin/mv', [
|
||||
path.join(projectDirectory, fileName),
|
||||
path.join(projectDirectory, destFileName),
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
async function _configureVersionsPlistAsync(configFilePath, standaloneSdkVersion) {
|
||||
await IosPlist.modifyAsync(configFilePath, 'EXSDKVersions', versionConfig => {
|
||||
versionConfig.sdkVersions = [standaloneSdkVersion];
|
||||
versionConfig.detachedNativeVersions = {
|
||||
shell: standaloneSdkVersion,
|
||||
kernel: standaloneSdkVersion,
|
||||
};
|
||||
return versionConfig;
|
||||
});
|
||||
}
|
||||
|
||||
async function _configureBuildConstantsPlistAsync(configFilePath, context) {
|
||||
await IosPlist.modifyAsync(configFilePath, 'EXBuildConstants', constantsConfig => {
|
||||
constantsConfig.STANDALONE_CONTEXT_TYPE = context.type;
|
||||
return constantsConfig;
|
||||
});
|
||||
}
|
||||
|
||||
async function _renderPodfileFromTemplateAsync(
|
||||
context,
|
||||
expoRootTemplateDirectory,
|
||||
sdkVersion,
|
||||
iosClientVersion
|
||||
) {
|
||||
const { iosProjectDirectory, projectName } = getPaths(context);
|
||||
let podfileTemplateFilename;
|
||||
const podfileSubstitutions = {
|
||||
TARGET_NAME: projectName,
|
||||
};
|
||||
let reactNativeDependencyPath;
|
||||
let modulesPath;
|
||||
|
||||
const detachableUniversalModules = Modules.getDetachableModules(
|
||||
'ios',
|
||||
context.data.shellAppSdkVersion || sdkVersion
|
||||
);
|
||||
if (context.type === 'user') {
|
||||
assert(iosClientVersion, `The iOS client version must be specified`);
|
||||
reactNativeDependencyPath = path.join(context.data.projectPath, 'node_modules', 'react-native');
|
||||
modulesPath = path.join(context.data.projectPath, 'node_modules');
|
||||
|
||||
podfileSubstitutions.EXPOKIT_TAG = `ios/${iosClientVersion}`;
|
||||
podfileTemplateFilename = 'ExpoKit-Podfile';
|
||||
} else if (context.type === 'service') {
|
||||
reactNativeDependencyPath = path.join(
|
||||
expoRootTemplateDirectory,
|
||||
'react-native-lab',
|
||||
'react-native'
|
||||
);
|
||||
modulesPath = path.join(expoRootTemplateDirectory, 'packages');
|
||||
|
||||
podfileSubstitutions.EXPOKIT_PATH = path.relative(
|
||||
iosProjectDirectory,
|
||||
expoRootTemplateDirectory
|
||||
);
|
||||
podfileSubstitutions.VERSIONED_REACT_NATIVE_PATH = path.relative(
|
||||
iosProjectDirectory,
|
||||
path.join(expoRootTemplateDirectory, 'ios', 'versioned-react-native')
|
||||
);
|
||||
podfileTemplateFilename = 'ExpoKit-Podfile-versioned';
|
||||
} else {
|
||||
throw new Error(`Unsupported context type: ${context.type}`);
|
||||
}
|
||||
podfileSubstitutions.REACT_NATIVE_PATH = path.relative(
|
||||
iosProjectDirectory,
|
||||
reactNativeDependencyPath
|
||||
);
|
||||
podfileSubstitutions.UNIVERSAL_MODULES_PATH = path.relative(iosProjectDirectory, modulesPath);
|
||||
podfileSubstitutions.UNIVERSAL_MODULES = detachableUniversalModules.map(module => ({
|
||||
...module,
|
||||
path: path.join(
|
||||
podfileSubstitutions.UNIVERSAL_MODULES_PATH,
|
||||
module.libName,
|
||||
module.subdirectory
|
||||
),
|
||||
}));
|
||||
|
||||
// env flags for testing
|
||||
if (process.env.EXPOKIT_TAG_IOS) {
|
||||
logger.info(`EXPOKIT_TAG_IOS: Using custom ExpoKit iOS tag...`);
|
||||
podfileSubstitutions.EXPOKIT_TAG = process.env.EXPOKIT_TAG_IOS;
|
||||
} else if (process.env.EXPO_VIEW_DIR) {
|
||||
logger.info('EXPO_VIEW_DIR: Using local ExpoKit source for iOS...');
|
||||
podfileSubstitutions.EXPOKIT_PATH = path.relative(
|
||||
iosProjectDirectory,
|
||||
process.env.EXPO_VIEW_DIR
|
||||
);
|
||||
// If EXPO_VIEW_DIR is defined overwrite UNIVERSAL_MODULES with paths pointing to EXPO_VIEW_DIR
|
||||
podfileSubstitutions.UNIVERSAL_MODULES = podfileSubstitutions.UNIVERSAL_MODULES.map(module => ({
|
||||
...module,
|
||||
path: path.relative(
|
||||
iosProjectDirectory,
|
||||
path.join(process.env.EXPO_VIEW_DIR, 'packages', module.libName, module.subdirectory)
|
||||
),
|
||||
}));
|
||||
}
|
||||
const templatePodfilePath = path.join(
|
||||
expoRootTemplateDirectory,
|
||||
'template-files',
|
||||
'ios',
|
||||
podfileTemplateFilename
|
||||
);
|
||||
await renderPodfileAsync(
|
||||
templatePodfilePath,
|
||||
path.join(iosProjectDirectory, 'Podfile'),
|
||||
podfileSubstitutions,
|
||||
context.data.shellAppSdkVersion,
|
||||
sdkVersion
|
||||
);
|
||||
}
|
||||
|
||||
async function createDetachedAsync(context) {
|
||||
const { iosProjectDirectory, projectName, supportingDirectory, projectRootDirectory } = getPaths(
|
||||
context
|
||||
);
|
||||
logger.info(`Creating ExpoKit workspace at ${iosProjectDirectory}...`);
|
||||
|
||||
const standaloneSdkVersion = await getNewestSdkVersionSupportedAsync(context);
|
||||
|
||||
let iosClientVersion;
|
||||
let iosExpoViewUrl;
|
||||
if (context.type === 'user') {
|
||||
({ iosClientVersion, iosExpoViewUrl } = await _getVersionedExpoKitConfigAsync(
|
||||
standaloneSdkVersion,
|
||||
process.env.EXPO_VIEW_DIR
|
||||
));
|
||||
}
|
||||
|
||||
const expoRootTemplateDirectory = await _getOrCreateTemplateDirectoryAsync(
|
||||
context,
|
||||
iosExpoViewUrl
|
||||
);
|
||||
|
||||
// copy template workspace
|
||||
logger.info('Moving iOS project files...');
|
||||
logger.info('Attempting to create project directory...');
|
||||
fs.mkdirpSync(iosProjectDirectory);
|
||||
logger.info('Created project directory! Copying files:');
|
||||
await Utils.ncpAsync(
|
||||
path.join(expoRootTemplateDirectory, 'exponent-view-template', 'ios'),
|
||||
iosProjectDirectory
|
||||
);
|
||||
|
||||
const projectPackageJsonPath = path.join(projectRootDirectory, 'package.json');
|
||||
|
||||
if (!(await fs.exists(projectPackageJsonPath))) {
|
||||
logger.info('Copying blank package.json...');
|
||||
await fs.copy(
|
||||
path.join(expoRootTemplateDirectory, 'exponent-view-template', 'package.json'),
|
||||
projectPackageJsonPath
|
||||
);
|
||||
}
|
||||
|
||||
logger.info('Installing required packages...');
|
||||
await _installRequiredPackagesAsync(projectRootDirectory, standaloneSdkVersion);
|
||||
|
||||
logger.info('Naming iOS project...');
|
||||
await _renameAndMoveProjectFilesAsync(context, iosProjectDirectory, projectName);
|
||||
|
||||
logger.info('Configuring iOS dependencies...');
|
||||
// this configuration must happen prior to build time because it affects which
|
||||
// native versions of RN we depend on.
|
||||
await _configureVersionsPlistAsync(supportingDirectory, standaloneSdkVersion);
|
||||
await _configureBuildConstantsPlistAsync(supportingDirectory, context);
|
||||
await _renderPodfileFromTemplateAsync(
|
||||
context,
|
||||
expoRootTemplateDirectory,
|
||||
standaloneSdkVersion,
|
||||
iosClientVersion
|
||||
);
|
||||
|
||||
if (!process.env.EXPO_VIEW_DIR) {
|
||||
if (context.type === 'user') {
|
||||
rimrafDontThrow(expoRootTemplateDirectory);
|
||||
}
|
||||
await IosPlist.cleanBackupAsync(supportingDirectory, 'EXSDKVersions', false);
|
||||
}
|
||||
}
|
||||
|
||||
async function _getPackagesToInstallWhenEjecting(sdkVersion) {
|
||||
const versions = await Versions.versionsAsync();
|
||||
return versions.sdkVersions[sdkVersion]
|
||||
? versions.sdkVersions[sdkVersion].packagesToInstallWhenEjecting
|
||||
: null;
|
||||
}
|
||||
|
||||
// @tsapeta: Temporarily copied from Detach._detachAsync. This needs to be invoked also when creating a shell app workspace
|
||||
// and not only when ejecting. These copies can be moved to one place if we decide to have just one flow for these two processes.
|
||||
async function _installRequiredPackagesAsync(projectRoot, sdkVersion) {
|
||||
const packagesToInstallWhenEjecting = await _getPackagesToInstallWhenEjecting(sdkVersion);
|
||||
const packagesToInstall = [];
|
||||
|
||||
if (packagesToInstallWhenEjecting && typeof packagesToInstallWhenEjecting === 'object') {
|
||||
Object.keys(packagesToInstallWhenEjecting).forEach(packageName => {
|
||||
packagesToInstall.push(`${packageName}@${packagesToInstallWhenEjecting[packageName]}`);
|
||||
});
|
||||
}
|
||||
if (packagesToInstall.length) {
|
||||
await installPackagesAsync(projectRoot, packagesToInstall);
|
||||
}
|
||||
}
|
||||
|
||||
function addDetachedConfigToExp(exp, context) {
|
||||
if (context.type !== 'user') {
|
||||
logger.warn(`Tried to modify exp for a non-user StandaloneContext, ignoring`);
|
||||
return exp;
|
||||
}
|
||||
const { supportingDirectory } = getPaths(context);
|
||||
exp.ios.publishBundlePath = path.relative(
|
||||
context.data.projectPath,
|
||||
path.join(supportingDirectory, 'shell-app.bundle')
|
||||
);
|
||||
exp.ios.publishManifestPath = path.relative(
|
||||
context.data.projectPath,
|
||||
path.join(supportingDirectory, 'shell-app-manifest.json')
|
||||
);
|
||||
return exp;
|
||||
}
|
||||
|
||||
/**
|
||||
* paths returned:
|
||||
* iosProjectDirectory - root directory of an (uncompiled) xcworkspace and obj-c source tree
|
||||
* projectName - xcworkspace project name normalized from context.config
|
||||
* supportingDirectory - location of Info.plist, xib files, etc. during configuration.
|
||||
* for an unbuilt app this is underneath iosProjectDirectory. for a compiled app it's just
|
||||
* a path to the flat xcarchive.
|
||||
* intermediatesDirectory - temporary spot to write whatever files are needed during the
|
||||
* detach/build process but can be discarded afterward.
|
||||
*/
|
||||
function getPaths(context) {
|
||||
let iosProjectDirectory;
|
||||
let projectName;
|
||||
let supportingDirectory;
|
||||
let projectRootDirectory;
|
||||
|
||||
if (context.build.isExpoClientBuild()) {
|
||||
projectName = 'Exponent';
|
||||
} else if (context.isAnonymous()) {
|
||||
projectName = 'ExpoKitApp';
|
||||
} else if (context.config && context.config.name) {
|
||||
const projectNameLabel = context.config.name;
|
||||
projectName = projectNameLabel.replace(/[^a-z0-9_-]/gi, '-').toLowerCase();
|
||||
} else {
|
||||
throw new Error('Cannot configure an Expo project with no name.');
|
||||
}
|
||||
if (context.type === 'user') {
|
||||
projectRootDirectory = context.data.projectPath;
|
||||
iosProjectDirectory = path.join(context.data.projectPath, 'ios');
|
||||
supportingDirectory = path.join(iosProjectDirectory, projectName, 'Supporting');
|
||||
} else if (context.type === 'service') {
|
||||
projectRootDirectory = path.dirname(context.build.ios.workspaceSourcePath);
|
||||
iosProjectDirectory = context.build.ios.workspaceSourcePath;
|
||||
if (context.data.archivePath) {
|
||||
// compiled archive has a flat NSBundle
|
||||
supportingDirectory = context.data.archivePath;
|
||||
} else {
|
||||
supportingDirectory = path.join(iosProjectDirectory, projectName, 'Supporting');
|
||||
}
|
||||
} else {
|
||||
throw new Error(`Unsupported StandaloneContext type: ${context.type}`);
|
||||
}
|
||||
// sandbox intermediates directory by workspace so that concurrently operating
|
||||
// contexts do not interfere with one another.
|
||||
const intermediatesDirectory = path.join(
|
||||
iosProjectDirectory,
|
||||
context.build.isExpoClientBuild() ? 'ExponentIntermediates' : 'ExpoKitIntermediates'
|
||||
);
|
||||
return {
|
||||
projectRootDirectory,
|
||||
intermediatesDirectory,
|
||||
iosProjectDirectory,
|
||||
projectName,
|
||||
supportingDirectory,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the newest sdk version supported given the standalone context.
|
||||
* Not all contexts support the newest sdk version.
|
||||
*/
|
||||
async function getNewestSdkVersionSupportedAsync(context) {
|
||||
if (context.type === 'user') {
|
||||
return context.data.exp.sdkVersion;
|
||||
} else if (context.type === 'service') {
|
||||
// when running in universe or on a turtle machine,
|
||||
// we care about what sdk version is actually present in this working copy.
|
||||
// this might not be the same thing deployed to our www Versions endpoint.
|
||||
let { supportingDirectory } = getPaths(context);
|
||||
if (!fs.existsSync(supportingDirectory)) {
|
||||
// if we run this method before creating the workspace, we may need to look at the template.
|
||||
supportingDirectory = path.join(
|
||||
context.data.expoSourcePath,
|
||||
'..',
|
||||
'exponent-view-template',
|
||||
'ios',
|
||||
'exponent-view-template',
|
||||
'Supporting'
|
||||
);
|
||||
}
|
||||
let allVersions, newestVersion;
|
||||
await IosPlist.modifyAsync(supportingDirectory, 'EXSDKVersions', versionConfig => {
|
||||
allVersions = versionConfig.sdkVersions;
|
||||
return versionConfig;
|
||||
});
|
||||
let highestMajorComponent = 0;
|
||||
allVersions.forEach(version => {
|
||||
const majorComponent = parseSdkMajorVersion(version);
|
||||
if (majorComponent > highestMajorComponent) {
|
||||
highestMajorComponent = majorComponent;
|
||||
newestVersion = version;
|
||||
}
|
||||
});
|
||||
return newestVersion;
|
||||
}
|
||||
}
|
||||
|
||||
export { addDetachedConfigToExp, createDetachedAsync, getPaths, getNewestSdkVersionSupportedAsync };
|
||||
@@ -0,0 +1,131 @@
|
||||
import bunyan from '@expo/bunyan';
|
||||
import isEmpty from 'lodash/isEmpty';
|
||||
import isPlainObject from 'lodash/isPlainObject';
|
||||
import { Readable } from 'stream';
|
||||
|
||||
export enum LogLevel {
|
||||
trace = 'trace',
|
||||
debug = 'debug',
|
||||
info = 'info',
|
||||
warn = 'warn',
|
||||
error = 'error',
|
||||
fatal = 'fatal',
|
||||
}
|
||||
|
||||
type BunyanGetter = () => bunyan;
|
||||
|
||||
export class Logger {
|
||||
public loggerObj: bunyan;
|
||||
public loggerGetter?: BunyanGetter;
|
||||
public extraFields: any;
|
||||
|
||||
constructor(bunyanGetter?: BunyanGetter, extraFields?: any) {
|
||||
this.loggerObj = bunyan.createLogger({ name: 'xdl-detach' });
|
||||
this.loggerGetter = bunyanGetter;
|
||||
this.extraFields = extraFields;
|
||||
}
|
||||
|
||||
public configure(loggerObj: bunyan) {
|
||||
this.loggerObj = loggerObj;
|
||||
}
|
||||
|
||||
public withFields(extraFields: any) {
|
||||
const getter = this.loggerGetter || (() => this.loggerObj);
|
||||
return new Logger(getter, { ...this.extraFields, ...extraFields });
|
||||
}
|
||||
|
||||
public trace(...all: any[]) {
|
||||
this.logLine(LogLevel.trace, ...all);
|
||||
}
|
||||
public debug(...all: any[]) {
|
||||
this.logLine(LogLevel.debug, ...all);
|
||||
}
|
||||
public info(...all: any[]) {
|
||||
this.logLine(LogLevel.info, ...all);
|
||||
}
|
||||
public warn(...all: any[]) {
|
||||
this.logLine(LogLevel.warn, ...all);
|
||||
}
|
||||
public error(...all: any[]) {
|
||||
this.logLine(LogLevel.error, ...all);
|
||||
}
|
||||
public fatal(...all: any[]) {
|
||||
this.logLine(LogLevel.fatal, ...all);
|
||||
}
|
||||
|
||||
public logLine(level: LogLevel, ...args: any[]) {
|
||||
const argsToLog = [...args];
|
||||
const extraFieldsFromArgsExist = isPlainObject(args[0]);
|
||||
const extraFieldsFromArgs = extraFieldsFromArgsExist ? args[0] : {};
|
||||
if (extraFieldsFromArgsExist) {
|
||||
argsToLog.shift();
|
||||
}
|
||||
const extraFields = { ...extraFieldsFromArgs, ...this.extraFields };
|
||||
if (!isEmpty(extraFields)) {
|
||||
argsToLog.unshift(extraFields);
|
||||
}
|
||||
|
||||
if (this.loggerGetter) {
|
||||
const loggerObj = this.loggerGetter();
|
||||
// @ts-ignore: type mismatch
|
||||
loggerObj[level](...argsToLog);
|
||||
} else {
|
||||
// @ts-ignore: type mismatch
|
||||
this.loggerObj[level](...argsToLog);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const LoggerDetach = new Logger();
|
||||
export default LoggerDetach;
|
||||
|
||||
export function pipeOutputToLogger(
|
||||
{ stdout, stderr }: { stdout?: Readable | null; stderr?: Readable | null } = {
|
||||
stdout: null,
|
||||
stderr: null,
|
||||
},
|
||||
extraFields = {},
|
||||
{
|
||||
stdoutOnly = false,
|
||||
loggerLineTransformer,
|
||||
}: { stdoutOnly?: boolean; loggerLineTransformer?: (line: any) => any } = {},
|
||||
) {
|
||||
if (stdout) {
|
||||
stdout.on('data', (chunk) =>
|
||||
logMultiline(
|
||||
chunk,
|
||||
{ ...extraFields, source: 'stdout' },
|
||||
loggerLineTransformer,
|
||||
),
|
||||
);
|
||||
}
|
||||
if (stderr) {
|
||||
const source = stdoutOnly ? 'stdout' : 'stderr';
|
||||
stderr.on('data', (chunk) =>
|
||||
logMultiline(chunk, { ...extraFields, source }, loggerLineTransformer),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function logMultiline(
|
||||
data: any,
|
||||
extraFields: any,
|
||||
loggerLineTransformer?: (line: any) => any,
|
||||
) {
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
const lines = String(data).split('\n');
|
||||
lines.forEach((line) => {
|
||||
const lineToPrint = loggerLineTransformer
|
||||
? loggerLineTransformer(line)
|
||||
: line;
|
||||
if (lineToPrint) {
|
||||
const args = [lineToPrint];
|
||||
if (!isEmpty(extraFields)) {
|
||||
args.unshift(extraFields);
|
||||
}
|
||||
LoggerDetach.info(...args);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import forge from 'node-forge';
|
||||
|
||||
export function getP12CertFingerprint(
|
||||
p12Buffer: Buffer | string,
|
||||
passwordRaw: string | null,
|
||||
): string {
|
||||
const certData = _getCertData(p12Buffer, passwordRaw);
|
||||
const certAsn1 = forge.pki.certificateToAsn1(certData);
|
||||
const certDer = forge.asn1.toDer(certAsn1).getBytes();
|
||||
return forge.md.sha1.create().update(certDer).digest().toHex().toUpperCase();
|
||||
}
|
||||
|
||||
export function findP12CertSerialNumber(
|
||||
p12Buffer: Buffer | string,
|
||||
passwordRaw: string | null,
|
||||
): string | null {
|
||||
const { serialNumber } = getCertData(p12Buffer, passwordRaw);
|
||||
return serialNumber;
|
||||
}
|
||||
|
||||
function _processSerialNumber(maybeSerialNumber: string | null | undefined) {
|
||||
return maybeSerialNumber ? maybeSerialNumber.replace(/^0+/, '').toUpperCase() : null;
|
||||
}
|
||||
|
||||
export function getCertData(p12Buffer: Buffer | string, passwordRaw: string | null) {
|
||||
const certData = _getCertData(p12Buffer, passwordRaw);
|
||||
return {
|
||||
...certData,
|
||||
serialNumber: _processSerialNumber(certData.serialNumber),
|
||||
};
|
||||
}
|
||||
|
||||
function _getCertData(p12Buffer: Buffer | string, passwordRaw: string | null) {
|
||||
if (Buffer.isBuffer(p12Buffer)) {
|
||||
p12Buffer = p12Buffer.toString('base64');
|
||||
} else if (typeof p12Buffer !== 'string') {
|
||||
throw new Error('_getCertData only takes strings and buffers.');
|
||||
}
|
||||
|
||||
const password = String(passwordRaw || '');
|
||||
const p12Der = forge.util.decode64(p12Buffer);
|
||||
const p12Asn1 = forge.asn1.fromDer(p12Der);
|
||||
const p12 = forge.pkcs12.pkcs12FromAsn1(p12Asn1, password);
|
||||
const certBagType = forge.pki.oids.certBag;
|
||||
const certData = p12.getBags({ bagType: certBagType })?.[certBagType]?.[0]?.cert;
|
||||
if (!certData) {
|
||||
throw new Error('_getCertData: couldn\'t find cert bag');
|
||||
}
|
||||
return certData;
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* StandaloneBuildFlags is owned by a StandaloneContext and carries information about
|
||||
* how to compile native code during the build step.
|
||||
*/
|
||||
|
||||
type StandaloneBuildConfiguration = 'Debug' | 'Release';
|
||||
interface IStandaloneBuildAndroidFlags {
|
||||
keystore: string;
|
||||
keystorePassword: string;
|
||||
keyAlias: string;
|
||||
keyPassword: string;
|
||||
outputFile: string | null;
|
||||
}
|
||||
interface IStandaloneBuildIosFlags {
|
||||
workspaceSourcePath: string;
|
||||
appleTeamId: string | null;
|
||||
buildType?: string;
|
||||
bundleExecutable?: string;
|
||||
}
|
||||
|
||||
class StandaloneBuildFlags {
|
||||
public static createEmpty = () => {
|
||||
return new StandaloneBuildFlags();
|
||||
}
|
||||
|
||||
public static createIos = (
|
||||
configuration: StandaloneBuildConfiguration,
|
||||
ios?: IStandaloneBuildIosFlags,
|
||||
): StandaloneBuildFlags => {
|
||||
const flags = new StandaloneBuildFlags();
|
||||
flags.configuration = configuration;
|
||||
flags.ios = ios;
|
||||
flags.isExpoClientBuild = () => ios?.buildType === 'client';
|
||||
return flags;
|
||||
}
|
||||
|
||||
public static createAndroid = (
|
||||
configuration: StandaloneBuildConfiguration,
|
||||
android?: IStandaloneBuildAndroidFlags,
|
||||
): StandaloneBuildFlags => {
|
||||
const flags = new StandaloneBuildFlags();
|
||||
flags.configuration = configuration;
|
||||
flags.android = android;
|
||||
flags.isExpoClientBuild = () => false;
|
||||
return flags;
|
||||
}
|
||||
public configuration: StandaloneBuildConfiguration = 'Debug';
|
||||
public android?: IStandaloneBuildAndroidFlags;
|
||||
public ios?: IStandaloneBuildIosFlags;
|
||||
public isExpoClientBuild: () => boolean = () => false;
|
||||
}
|
||||
|
||||
export default StandaloneBuildFlags;
|
||||
@@ -0,0 +1,125 @@
|
||||
import StandaloneBuildFlags from './StandaloneBuildFlags';
|
||||
import { StandaloneContextService } from './StandaloneContextService';
|
||||
import { StandaloneContextUser } from './StandaloneContextUser';
|
||||
|
||||
export type StandaloneContextDataType = 'user' | 'service';
|
||||
|
||||
export type StandaloneContextTestEnvironment = 'none' | 'local' | 'ci';
|
||||
|
||||
// currently unused
|
||||
export function isStandaloneContextDataUser(
|
||||
value: any,
|
||||
): value is IStandaloneContextDataUser {
|
||||
return value && typeof value.projectPath === 'string' && 'exp' in value;
|
||||
}
|
||||
|
||||
export function isStandaloneContextTestEnvironment(
|
||||
value: string,
|
||||
): value is StandaloneContextTestEnvironment {
|
||||
return ['none', 'local', 'ci'].includes(value);
|
||||
}
|
||||
|
||||
export function isStandaloneContextDataService(
|
||||
value: any,
|
||||
): value is IStandaloneContextDataService {
|
||||
return (
|
||||
value &&
|
||||
isStandaloneContextTestEnvironment(value.testEnvironment) &&
|
||||
typeof value.expoSourcePath === 'string' &&
|
||||
typeof value.shellAppSdkVersion === 'string'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* A user context is used when we are configuring a standalone app locally on a user's machine,
|
||||
* such as during `exp detach`.
|
||||
*/
|
||||
export interface IStandaloneContextDataUser {
|
||||
projectPath: string;
|
||||
exp: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* A service context is used when we are generating a standalone app remotely on an Expo
|
||||
* service machine, such as during `exp build`.
|
||||
*/
|
||||
export interface IStandaloneContextDataService {
|
||||
expoSourcePath: string;
|
||||
archivePath: string | null;
|
||||
manifest: any;
|
||||
privateConfig: any;
|
||||
testEnvironment: StandaloneContextTestEnvironment;
|
||||
shellAppSdkVersion: string;
|
||||
}
|
||||
|
||||
export class StandaloneContext {
|
||||
public static createUserContext = (
|
||||
projectPath: string,
|
||||
exp: any,
|
||||
publishedUrl?: string,
|
||||
): StandaloneContextUser => {
|
||||
const context = new StandaloneContextUser(
|
||||
{
|
||||
projectPath,
|
||||
exp,
|
||||
},
|
||||
{
|
||||
url: publishedUrl,
|
||||
releaseChannel: 'default',
|
||||
},
|
||||
// we never expect to handle the build step for user contexts right now
|
||||
StandaloneBuildFlags.createEmpty(),
|
||||
);
|
||||
context.config = exp;
|
||||
return context;
|
||||
}
|
||||
|
||||
public static createServiceContext = (
|
||||
expoSourcePath: string,
|
||||
archivePath: string | null,
|
||||
manifest: any,
|
||||
privateConfig: any,
|
||||
testEnvironment: StandaloneContextTestEnvironment,
|
||||
build: StandaloneBuildFlags,
|
||||
publishedUrl: string,
|
||||
releaseChannel: string,
|
||||
shellAppSdkVersion: string,
|
||||
): StandaloneContextService => {
|
||||
const context = new StandaloneContextService(
|
||||
{
|
||||
expoSourcePath,
|
||||
archivePath,
|
||||
manifest,
|
||||
privateConfig,
|
||||
testEnvironment,
|
||||
shellAppSdkVersion,
|
||||
},
|
||||
{
|
||||
url: publishedUrl,
|
||||
releaseChannel: releaseChannel ? releaseChannel : 'default',
|
||||
},
|
||||
build,
|
||||
);
|
||||
context.config = manifest;
|
||||
|
||||
return context;
|
||||
}
|
||||
public data?: IStandaloneContextDataUser | IStandaloneContextDataService;
|
||||
public config: any; // same as underlying app.json or manifest
|
||||
|
||||
/**
|
||||
* On iOS we begin configuring standalone apps before we have any information about the
|
||||
* project's manifest. By default let's treat all contexts as non-anonymous and override
|
||||
* it in contexts that needs this to be different.
|
||||
*/
|
||||
public isAnonymous() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
export type AnyStandaloneContext =
|
||||
| StandaloneContextUser
|
||||
| StandaloneContextService;
|
||||
|
||||
export default StandaloneContext;
|
||||
|
||||
export { StandaloneContextService, StandaloneContextUser };
|
||||
Loaded 100 of 109 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user