Compare commits

..
Author SHA1 Message Date
Evan Bacon 6d8726a1fc copied turtle related xdl files over 2021-01-27 18:02:31 -08:00
Wojciech Kozyra 1d773f35cf Release 0.20.7 2021-01-26 14:49:57 +01:00
Wojciech Kozyra 3daa678540 [android] remove Go suffix from android app name (#292) 2021-01-26 14:20:33 +01:00
Brent Vatne 616331b47b update changelog 2021-01-25 11:39:45 -08:00
Brent Vatne 0b9ab78453 Release 0.20.6 2021-01-25 11:36:30 -08:00
Brent Vatne 3e345ddaf8 [ios][android] Update tarballs for SDK 40 patch release 2021-01-21 20:40:43 -08:00
Cedric van Putten 4866c06a02 update changelog 2020-12-23 18:28:28 +01:00
Cedric van Putten c36723d96c Release 0.20.5 2020-12-23 18:25:36 +01:00
Cedric van Putten 28b0266ba2 [android] Upgrade to client 2.18.5 with background permission fix (#287) 2020-12-23 15:07:09 +01:00
Stanisław Chmiela 84a19ee261 Release 0.20.4 2020-12-17 15:57:42 +01:00
Stanisław Chmiela 8f3be65326 [ios] Fix expo-notifications initial notification not being delivered… (#285)
to expo-notifications

<!-- Thanks for contributing to _turtle_! Before you submit your pull request, please make sure to check the following boxes by putting an x in the [ ] (don't: [x ], [ x], do: [x]) -->

### Checklist
- [x] I've read the [Contribution Guidelines](https://github.com/expo/turtle/blob/master/CONTRIBUTING.md).
- [x] I've updated the [CHANGELOG](https://github.com/expo/turtle/blob/master/CHANGELOG.md) if necessary.
- [x] I've ensured the unit and smoke tests are still passing - either by running `yarn test:unit` and `yarn test:smoke:[ios|android]` or by checking the appropriate CircleCI builds' statuses.
- [x] **I've manually tested whether the changes I made work as expected.**

### Motivation and Context
Adds https://github.com/expo/expo/commit/0e0bbdd7b45b3e8f8725bcd831263f049c0aa16a and https://github.com/expo/expo/commit/c49a0464aef589dc9dd691cea78313d91959d64c fixing long-standing notifications bug.
2020-12-17 15:56:39 +01:00
Wojciech Kozyra 5d539a1bb2 update CHANGELOG 2020-12-10 12:11:09 +01:00
Wojciech Kozyra 833c9c1de5 Release 0.20.3 2020-12-10 12:09:10 +01:00
Eric Gonzalez ce74ab3161 Fixed the iOS build when user does not have elevated permissions (#283)
* Removed unnecessary sudo usage for ios buildsudo does not appear to be necessary to execute 'xcrun simctl list'

* Conditionally sudo-ing xcrun when building online
2020-12-10 10:47:25 +01:00
Brent Vatne 62df247645 [scripts] Verify jq installation before proceeding (#281) 2020-12-09 20:57:57 -08:00
Stanisław Chmiela 9bc4d75e9b Cut 0.20.2 version in changelog 2020-12-09 16:45:33 +01:00
Stanisław Chmiela b4d4b8a6c3 Release 0.20.2 2020-12-09 16:39:20 +01:00
Stanisław Chmiela 88d75c7581 Add changelog entry 2020-12-09 16:38:07 +01:00
Stanisław Chmiela 03be505cd0 [android] Upgrade SDK40 tarball with expo-notifications installation identifier fix 2020-12-09 15:58:41 +01:00
Eric Samelson 05568676ee update CHANGELOG 2020-12-08 18:54:00 -08:00
Eric Samelson 903e44bd29 Release 0.20.1 2020-12-08 18:51:55 -08:00
Brent Vatne 7697542b44 Update iOS client tarball 2020-12-08 18:44:45 -08:00
Eric Samelson 3baf4bcade [android] update SDK 40 shell tarball for release 2020-12-08 17:34:44 -08:00
Brent Vatne c7e98c5858 [ios] Update SDK 40 tarball for release 2020-12-08 13:12:26 -08:00
Dominik Sokal fc48d27e42 fix android smoke tests 2020-12-01 14:12:33 +01:00
Brent Vatne fdc702f273 Update CHANGELOG 2020-11-30 13:37:03 -08:00
Brent Vatne 9fda74dfa4 Release 0.20.0 2020-11-30 13:26:37 -08:00
Brent Vatne 6bab5f71ef [README] Update link to installation docs 2020-11-30 11:02:28 -08:00
Brent Vatne 3f1e9ce910 [android] Update sdk40 tarball 2020-11-30 11:01:37 -08:00
Brent Vatne 8fb54f3f27 Add iOS and Android SDK 40 tarballs 2020-11-27 11:53:29 -08:00
Bartłomiej Bukowski 069ef7e1c9 Update CHANGELOG.md 2020-11-19 21:23:53 +01:00
Bartłomiej Bukowski c0a742fa21 Release 0.19.1 2020-11-19 21:13:23 +01:00
Bartłomiej Bukowski fca72875dc [iOS] Rebuild shell tarballs (#278)
### Motivation and Context

- Accommodate https://github.com/expo/expo/pull/11052 in all shellApps.
- Rebuild iOS shellApps using Xcode 12.1 containing the iOS 14 image fix.
2020-11-19 21:02:25 +01:00
Bartłomiej Bukowski 15ed26d4cd [iOS] Run 'xcrun simctl list' before build to prevent ibtool crash (#279)
* [iOS] Run 'xcrun simctl list' before build to prevent ibtool crash

* Update CHANGELOG
2020-11-18 11:15:42 +01:00
Wojciech Kozyra 4c7af801f1 Revert "[ios][shellApps] Rebuild all supported shellApps with Xcode 12.1 (#273)"
This reverts commit c6946eca25.
2020-11-16 18:16:31 +01:00
Wojciech Kozyra 5012ec669d update CHANGELOG.md 2020-11-13 13:12:27 +01:00
Wojciech Kozyra 8b77a48b82 Release 0.19.0 2020-11-13 13:10:36 +01:00
Bartłomiej BukowskiandWojciech Kozyra c6946eca25 [ios][shellApps] Rebuild all supported shellApps with Xcode 12.1 (#273)
Co-authored-by: Wojciech Kozyra <[email protected]>
2020-11-11 12:08:25 +01:00
Eric Samelson 9627ecb80c Update CHANGELOG for 0.18.9 2020-11-09 16:24:03 -08:00
Eric Samelson 48c78f386a Release 0.18.9 2020-11-09 16:22:05 -08:00
Eric Samelson 0dc251e023 bump xdl to 58.0.20 2020-11-09 16:11:26 -08:00
Stanisław Chmiela d984061134 Update changelog for 0.18.8 2020-10-27 13:00:10 +01:00
Stanisław Chmiela ff6401ba6b Release 0.18.8 2020-10-27 12:59:02 +01:00
Stanisław Chmiela b443760699 Fix Reanimated v2 not being configured in production mode (#270)
<!-- Thanks for contributing to _turtle_! Before you submit your pull request, please make sure to check the following boxes by putting an x in the [ ] (don't: [x ], [ x], do: [x]) -->

### Checklist
- [x] I've read the [Contribution Guidelines](https://github.com/expo/turtle/blob/master/CONTRIBUTING.md).
- [x] I've updated the [CHANGELOG](https://github.com/expo/turtle/blob/master/CHANGELOG.md) if necessary.
- [x] I've ensured the unit and smoke tests are still passing - either by running `yarn test:unit` and `yarn test:smoke:[ios|android]` or by checking the appropriate CircleCI builds' statuses.
- [x] **I've manually tested whether the changes I made work as expected.**

### Motivation and Context

Apply https://github.com/expo/expo/pull/10807.

### Description
Fixes app crashing when using Reanimated v2.
2020-10-27 12:57:06 +01:00
Eric Samelson 0f0e86d17f Release 0.18.7 2020-10-23 15:28:41 -07:00
Eric Samelson f6697c0029 update CHANGELOG 2020-10-23 15:28:14 -07:00
Eric Samelson 22f802164d update android sdk 39 tarball 2020-10-23 14:53:13 -07:00
Charlie Cruzan aaa6e61061 Update CHANGELOG.md 2020-10-22 17:17:43 -04:00
cruzach e532e0270c Release 0.18.6 2020-10-22 17:13:42 -04:00
Charlie Cruzan bc0718ca7d [android] update sdk 39 tarball with notifications fixes
the following fixes are added in this update:

- https://github.com/expo/expo/commit/6ffe64f4a75f75edfdae226574842e97fd63d523

- https://github.com/expo/expo/commit/fa659fd3f74c32e989db5920a3e004bee58a931c

- https://github.com/expo/expo/commit/0faa400c630a5f88a029fe87c84cfa9e684e800a
2020-10-22 13:54:30 -04:00
Brent Vatne bf63dcea70 Update CHANGELOG 2020-10-07 12:34:43 -07:00
Brent Vatne 9551c8773d Release 0.18.5 2020-10-07 12:33:14 -07:00
Brent Vatne 470ff609fd [android] Always add expo-notifications for SDK >= 39 projects with android.enableDangerousExperimentalLeanBuilds enabled (#267) 2020-10-07 10:29:50 -07:00
Eric Samelson addba74b9d Release 0.18.4 2020-10-06 18:38:53 -07:00
Eric Samelson b63e9e3f34 update CHANGELOG 2020-10-06 18:37:48 -07:00
Eric Samelson 25aaa849f1 [android] update sdk 39 tarball for android notifications/updates fix 2020-10-06 18:36:22 -07:00
Brent Vatne edc712d820 update CHANGELOG 2020-10-03 22:51:11 -07:00
Brent Vatne 23c3e3509c Release 0.18.3 2020-10-03 22:48:52 -07:00
Brent Vatne 406a3be4f1 Update @expo/xdl to fix regression in SDK <= 39 splash 2020-10-03 22:15:55 -07:00
Eric Samelson dd60721270 update CHANGELOG 2020-10-03 15:19:38 -07:00
Eric Samelson 469ead8df8 Release 0.18.2 2020-10-03 15:18:25 -07:00
Brent Vatne aa0ce81e98 [ios] Update client tarball 2020-10-03 15:15:47 -07:00
Eric Samelson dccccb7716 [android] update SDK 39 tarball with linking fix 2020-10-03 15:10:09 -07:00
Brent Vatne 57f8404b6d [ios] update sdk39 tarball 2020-10-02 22:09:09 -07:00
Brent Vatne ba7309bc4b Update @expo/config and @expo/xdl. Temporarily bring in type definitions for color-string and xcode. 2020-10-02 20:58:39 -07:00
Brent Vatne 4aec3d5ba7 Update CHANGELOG 2020-10-02 18:40:08 -07:00
Brent Vatne d2eb447ebc [android] update sdk39 tarball 2020-10-02 18:16:21 -07:00
Dominik Sokal f177cf4d7f update ios smoke tests recordings (#264) 2020-09-29 15:31:52 +02:00
Dominik Sokal 59a181dee0 update CHANGELOG 2020-09-23 12:16:02 +02:00
Dominik Sokal f4bf860a9a Release 0.18.1 2020-09-23 12:12:59 +02:00
Dominik Sokal 932958ad46 fix building expo sdk 39 with turtle-cli 2020-09-23 12:09:45 +02:00
Eric Samelson 2a54b4578a update CHANGELOG 2020-09-21 15:08:37 -07:00
Eric Samelson 7e234d09a3 Release 0.18.0 2020-09-21 15:07:47 -07:00
Eric Samelson 5b22c351bf update CHANGELOG 2020-09-21 13:20:39 -07:00
Stanisław Chmiela e2020f1ab4 Add SDK39 iOS tarballs (#257) 2020-09-21 13:19:47 -07:00
Eric Samelson c935e53e50 [android][ios] remove SDK 35 tarballs 2020-09-18 15:14:20 -07:00
Eric Samelson 14e681f35f [android] add SDK 39 tarball (#260) 2020-09-18 15:13:32 -07:00
Wojciech Kozyra d3a7ccb582 force image rebuild 2020-09-18 12:34:14 +02:00
Wojciech Kozyra 62478de14a update CHANGELOG.md 2020-09-14 14:22:07 +02:00
Wojciech Kozyra 6594089779 Release 0.17.3 2020-09-14 14:19:59 +02:00
dependabot[bot] 11c5d3ee02 Bump http-proxy from 1.17.0 to 1.18.1 (#256)
Bumps [http-proxy](https://github.com/http-party/node-http-proxy) from 1.17.0 to 1.18.1.
- [Release notes](https://github.com/http-party/node-http-proxy/releases)
- [Changelog](https://github.com/http-party/node-http-proxy/blob/master/CHANGELOG.md)
- [Commits](https://github.com/http-party/node-http-proxy/compare/1.17.0...1.18.1)

Signed-off-by: dependabot[bot] <[email protected]>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-09-11 12:21:47 +02:00
dependabot[bot] 99b7ac6660 Bump node-fetch from 2.3.0 to 2.6.1 (#259)
Bumps [node-fetch](https://github.com/bitinn/node-fetch) from 2.3.0 to 2.6.1.
- [Release notes](https://github.com/bitinn/node-fetch/releases)
- [Changelog](https://github.com/node-fetch/node-fetch/blob/master/docs/CHANGELOG.md)
- [Commits](https://github.com/bitinn/node-fetch/compare/v2.3.0...v2.6.1)

Signed-off-by: dependabot[bot] <[email protected]>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-09-11 12:21:26 +02:00
Stanisław Chmiela 46fe34ed9c Hardcode maximum heap size (#255)
<!-- Thanks for contributing to _turtle_! Before you submit your pull request, please make sure to check the following boxes by putting an x in the [ ] (don't: [x ], [ x], do: [x]) -->

### Checklist
- [x] I've read the [Contribution Guidelines](https://github.com/expo/turtle/blob/master/CONTRIBUTING.md).
- [ ] I've updated the [CHANGELOG](https://github.com/expo/turtle/blob/master/CHANGELOG.md) if necessary.
- [x] I've ensured the unit and smoke tests are still passing - either by running `yarn test:unit` and `yarn test:smoke:[ios|android]` or by checking the appropriate CircleCI builds' statuses.
- [x] **I've manually tested whether the changes I made work as expected.**

### Motivation and Context

Builds started to fail with

```
Error occurred during initialization of VM
Initial heap size set to a larger value than the maximum heap size
Error: ./gradlew exited with non-zero code: 1
    at ChildProcess.completionListener (/app/turtle/node_modules/@expo/xdl/node_modules/@expo/spawn-async/build/spawnAsync.js:52:23)
    at Object.onceWrapper (events.js:418:26)
    at ChildProcess.emit (events.js:311:20)
    at ChildProcess.EventEmitter.emit (domain.js:482:12)
    at maybeClose (internal/child_process.js:1021:16)
    at Process.ChildProcess._handle.onexit (internal/child_process.js:286:5)
    ...
    at spawnAsync (/app/turtle/node_modules/@expo/xdl/node_modules/@expo/spawn-async/build/spawnAsync.js:17:21)
    at spawnAsyncThrowError (/app/turtle/node_modules/@expo/xdl/build/detach/ExponentTools.js:201:45)
    at buildShellAppAsync (/app/turtle/node_modules/@expo/xdl/build/detach/AndroidShellApp.js:940:11)
    at async Object.createAndroidShellAppAsync (/app/turtle/node_modules/@expo/xdl/build/detach/AndroidShellApp.js:386:5)
    at async runShellAppBuilder (/app/turtle/build/builders/android.js:95:9)
    at async Object.buildAndroid [as android] (/app/turtle/build/builders/android.js:43:28)
    at async build (/app/turtle/build/jobManager.js:181:33)
    at async processJob (/app/turtle/build/jobManager.js:118:32)
    at async Object.doJob (/app/turtle/build/jobManager.js:49:5)
    at async main (/app/turtle/build/server.js:66:13)
```

### Description

Consulted and helped with by @wkozyra95 — removed experimental options that made maximum heap size being set to a low value. Hardcoded proper value.
2020-09-07 14:05:02 +02:00
Stanisław Chmiela 423fc8bbf5 Upgrade XDL so it processes root build.gradle too (#254)
<!-- Thanks for contributing to _turtle_! Before you submit your pull request, please make sure to check the following boxes by putting an x in the [ ] (don't: [x ], [ x], do: [x]) -->

### Checklist
- [x] I've read the [Contribution Guidelines](https://github.com/expo/turtle/blob/master/CONTRIBUTING.md).
- [x] I've updated the [CHANGELOG](https://github.com/expo/turtle/blob/master/CHANGELOG.md) if necessary.
- [x] I've ensured the unit and smoke tests are still passing - either by running `yarn test:unit` and `yarn test:smoke:[ios|android]` or by checking the appropriate CircleCI builds' statuses.
- [x] **I've manually tested whether the changes I made work as expected.**

### Motivation and Context

Lands https://github.com/expo/expo-cli/pull/2547 in Turtle. This is a prerequisite for https://github.com/expo/expo/pull/10059 to be able to land (otherwise Turtle would end up with the offending block in root `build.gradle` which would fail the builds since there is no `:ReactAndroid` project in Turtle workspace).

### Description

Upgrades XDL.
2020-09-04 13:02:32 +02:00
Wojciech Kozyra 4c8408a46b [docker-base] add gradle 6.2 and set ANDROID_NDK_HOME (#251)
* [docker-base] add gradle 6.2 and set ANDROID_NDK_HOME

* update CHANGELOG
2020-09-04 11:19:08 +02:00
dependabot[bot] 032d7c8b1f Bump decompress from 4.2.0 to 4.2.1 (#252)
Bumps [decompress](https://github.com/kevva/decompress) from 4.2.0 to 4.2.1.
- [Release notes](https://github.com/kevva/decompress/releases)
- [Commits](https://github.com/kevva/decompress/compare/v4.2.0...v4.2.1)

Signed-off-by: dependabot[bot] <[email protected]>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-09-04 09:36:01 +02:00
Dominik Sokal b2a0b3ce14 use http endpoints for status updates (#249)
* use http endpoints for status updates

* resend updates if www failed to process them

* make leader responsible for synchronizing build updates

* fix tests
2020-08-24 10:13:43 +02:00
Dominik Sokal 1ee922763e send logs url with finished and errored messages (#248) 2020-08-17 13:43:40 +02:00
Wojciech Kozyra c31cd379c1 update CHANGELOG.md 2020-08-06 18:00:17 +02:00
Wojciech Kozyra 67754eba2a Release 0.17.2 2020-08-06 17:57:15 +02:00
Wojciech Kozyra 351fed00df Updated iOS shell app for SDK 38 with fixed integer tagging in expo-gl for iOS 14. (#245) 2020-08-06 17:51:49 +02:00
Stanisław Chmiela 487032b11d Update version manually as release-it broke 2020-08-05 18:07:03 +02:00
Stanisław Chmiela 042dd2ccab Add CHANGELOG entry 2020-08-05 16:12:55 +02:00
Stanisław Chmiela 99ae0d1cea Update Android shell tarball for SDK38 with targetSdkVersion bumped to 29 (#244)
<!-- Thanks for contributing to _turtle_! Before you submit your pull request, please make sure to check the following boxes by putting an x in the [ ] (don't: [x ], [ x], do: [x]) -->

### Checklist
- [x] I've read the [Contribution Guidelines](https://github.com/expo/turtle/blob/master/CONTRIBUTING.md).
- [x] I've updated the [CHANGELOG](https://github.com/expo/turtle/blob/master/CHANGELOG.md) if necessary.
- [x] I've ensured the unit and smoke tests are still passing - either by running `yarn test:unit` and `yarn test:smoke:[ios|android]` or by checking the appropriate CircleCI builds' statuses.
- [x] **I've manually tested whether the changes I made work as expected.**

### Motivation and Context

See https://github.com/expo/expo/pull/9446 and https://forums.expo.io/t/google-api-level-29/41231.

### Description

Updated Android SDK38 tarball with project with updated `targetSdkVersion`.
2020-08-05 16:06:46 +02:00
dependabot[bot] ad2c87163b Bump elliptic from 6.4.1 to 6.5.3 (#243)
Bumps [elliptic](https://github.com/indutny/elliptic) from 6.4.1 to 6.5.3.
- [Release notes](https://github.com/indutny/elliptic/releases)
- [Commits](https://github.com/indutny/elliptic/compare/v6.4.1...v6.5.3)

Signed-off-by: dependabot[bot] <[email protected]>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-07-31 12:12:03 +02:00
dependabot[bot] b4b9cf362e Bump lodash from 4.17.11 to 4.17.19 (#242)
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.11 to 4.17.19.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.11...4.17.19)

Signed-off-by: dependabot[bot] <[email protected]>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-07-17 09:08:55 +02:00
Dominik Sokal 7ea4dac986 upgrade jest and ts-jest to fix smoke tests (#241) 2020-07-16 14:44:59 +02:00
Dominik Sokal cd8da71711 update CHANGELOG 2020-07-16 08:54:06 +02:00
Dominik Sokal 2a2a0f39e0 Release 0.17.0 2020-07-16 08:51:04 +02:00
Dominik Sokal fbabcdf521 build:android - allow for passing custom gradle args (#239)
* build:android - allow for passing custom gradle args

* update CHANGELOG

* upgrade @expo/xdl to 57.9.25-alpha.1
2020-07-15 11:50:21 +02:00
Dominik Sokal 0803811226 update @expo/xdl to 57.9.25-alpha.0 to get rid of dots when downloading gradle 2020-07-15 10:21:45 +02:00
dependabot[bot] 0976a642e1 Bump npm-registry-fetch from 8.0.0 to 8.1.1 (#238)
Bumps [npm-registry-fetch](https://github.com/npm/registry-fetch) from 8.0.0 to 8.1.1.
- [Release notes](https://github.com/npm/registry-fetch/releases)
- [Changelog](https://github.com/npm/npm-registry-fetch/blob/latest/CHANGELOG.md)
- [Commits](https://github.com/npm/registry-fetch/compare/v8.0.0...v8.1.1)

Signed-off-by: dependabot[bot] <[email protected]>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-07-08 10:00:37 +02:00
Dominik Sokal 16bd4fe7b6 run yarn before release 2020-07-01 09:38:08 +02:00
Dominik Sokal 3ccd705943 Release 0.16.2 2020-07-01 09:35:06 +02:00
97 changed files with 10870 additions and 7393 deletions

No files matched your search

+8 -8
View File
@@ -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:
+3
View File
@@ -10,6 +10,9 @@
"release": true
},
"hooks": {
"before:init": [
"yarn"
],
"after:release": "yarn release:update:turtle-cli-example"
}
}
+156
View File
@@ -7,6 +7,162 @@ 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`.
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+13
View File
@@ -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
+2 -7
View File
@@ -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,9 @@ 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:
+34 -8
View File
@@ -1,6 +1,6 @@
{
"name": "turtle-cli",
"version": "0.16.1",
"version": "0.20.7",
"description": "The command-line tool for building standalone Expo apps",
"preferGlobal": true,
"scripts": {
@@ -67,16 +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",
@@ -86,15 +90,16 @@
"@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",
@@ -104,12 +109,21 @@
"typescript": "^3.9.5"
},
"dependencies": {
"@expo/config": "^3.2.14",
"@expo/spawn-async": "^1.4.2",
"@expo/xdl": "^57.9.13",
"@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",
@@ -121,18 +135,30 @@
"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"
+1 -3
View File
@@ -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
+6
View File
@@ -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
+6
View File
@@ -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
+1 -1
View File
@@ -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
+1 -2
View File
@@ -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
-1
View File
@@ -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"
-1
View File
@@ -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"
+6
View File
@@ -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
View File
@@ -1 +0,0 @@
s3://exp-artifacts/android-shell-builder-7ce098f40cc82a2f61fed9f1f515d695a83c55b2.tar.gz
+1 -1
View File
@@ -1 +1 @@
s3://exp-artifacts/android-shell-builder-076a13fd8b4ee84a59eb6d2777f6db0a2c71fb6d.tar.gz
s3://exp-artifacts/android-shell-builder-6cde8e00cd7bdf7a933cd8aad7f8e86981ea68a2.tar.gz
+1
View File
@@ -0,0 +1 @@
s3://exp-artifacts/android-shell-builder-d292dcd0cc277ab665957244a8b15b9981debd29.tar.gz
+1
View File
@@ -0,0 +1 @@
s3://exp-artifacts/android-shell-builder-277393d97877f56ba3639ab153c69f109d248436.tar.gz
+1 -1
View File
@@ -1 +1 @@
s3://exp-artifacts/ios-expo-client-18a58afe742e63f1d2ac71ba21404d8b84e6563e.tar.gz
s3://exp-artifacts/ios-expo-client-dd6b6b4840c7a99725f1ec9a9a3970de91e90b1d.tar.gz
-1
View File
@@ -1 +0,0 @@
s3://exp-artifacts/ios-shell-builder-sdk-latest-0115c48ddac4f63e5215541dff804871ef8c1e03.tar.gz
+1 -1
View File
@@ -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
View File
@@ -1 +1 @@
s3://exp-artifacts/ios-shell-builder-sdk-latest-30eb0558ff6a77acf56bb045e98b84f6a5e1478a.tar.gz
s3://exp-artifacts/ios-shell-builder-sdk-latest-df407aadb7208fc389564aed400c39263294eb76.tar.gz
+1 -1
View File
@@ -1 +1 @@
s3://exp-artifacts/ios-shell-builder-sdk-latest-b0acfb129b4526f771bd0b363f2896b35f007e83.tar.gz
s3://exp-artifacts/ios-shell-builder-sdk-latest-ba6ec1974977bebf8fd2db22a1c87f6a0b36ffa5.tar.gz
+1
View File
@@ -0,0 +1 @@
s3://exp-artifacts/ios-shell-builder-sdk-latest-4a9a038998f6be13e8a2900a95afbb9f96dd023d.tar.gz
+1
View File
@@ -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>{&quot;id&quot;:&quot;1b148a0a-4a5e-4572-b28a-c31aa018351a&quot;,&quot;experienceId&quot;:&quot;0128bca1-78cf-4cb8-ac00-c07600500254&quot;,&quot;experienceName&quot;:&quot;@dsokal-local/turtle-cli-example&quot;,&quot;platform&quot;:&quot;android&quot;,&quot;config&quot;:{&quot;releaseChannel&quot;:&quot;default&quot;,&quot;buildType&quot;:&quot;apk&quot;},&quot;status&quot;:&quot;sent-to-queue&quot;,&quot;sdkVersion&quot;:&quot;37.0.0&quot;,&quot;accountId&quot;:&quot;9b473c78-6d0e-4b69-88a8-1c0960287b2f&quot;,&quot;priority&quot;:&quot;normal&quot;,&quot;credentials&quot;:{&quot;keystore&quot;:&quot;/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>{&quot;id&quot;:&quot;686530e4-c025-4a01-b84b-041e6d4f7fb3&quot;,&quot;experienceId&quot;:&quot;4d928c1c-5be9-4380-8fd5-48a6a4805839&quot;,&quot;experienceName&quot;:&quot;@testtest2/sdk-39-test&quot;,&quot;platform&quot;:&quot;android&quot;,&quot;config&quot;:{&quot;releaseChannel&quot;:&quot;default&quot;,&quot;buildType&quot;:&quot;apk&quot;},&quot;status&quot;:&quot;sent-to-queue&quot;,&quot;sdkVersion&quot;:&quot;39.0.0&quot;,&quot;accountId&quot;:&quot;1ecd48ee-e393-4f6b-9f77-311f2e0d2b39&quot;,&quot;priority&quot;:&quot;high&quot;,&quot;credentials&quot;:{&quot;keystore&quot;:&quot;/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
View File
@@ -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>{&quot;id&quot;:&quot;883f68d1-398a-4327-83da-6de1f288456d&quot;,&quot;experienceId&quot;:&quot;2945b084-8989-4a02-b781-2a620034a172&quot;,&quot;experienceName&quot;:&quot;@dsokal/turtle-cli-example&quot;,&quot;platform&quot;:&quot;ios&quot;,&quot;config&quot;:{&quot;releaseChannel&quot;:&quot;default&quot;,&quot;usesNonExemptEncryption&quot;:false,&quot;buildType&quot;:&quot;archive&quot;,&quot;bundleIdentifier&quot;:&quot;com.expo.turtle.cli.example&quot;},&quot;status&quot;:&quot;sent-to-queue&quot;,&quot;sdkVersion&quot;:&quot;39.0.0&quot;,&quot;accountId&quot;:&quot;07eb1118-80a9-4769-a9b5-9f429cab29e2&quot;,&quot;priority&quot;:&quot;normal&quot;,&quot;credentials&quot;:{&quot;apnsKeyId&quot;:&quot;54G5NV49UY&quot;,&quot;apnsKeyP8&quot;:&quot;-----BEGIN PRIVATE KEY-----\\nMIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQgBtkLIodAlsdUmV1L\\nxirn9Erof29278yzTaR/NNlvF0igCgYIKoZIzj0DAQehRANCAAS5jlfFX2KmTFi/\\ng+aohp77kCfYRZNYYmn4cgn4dKkXu5YOGbTJlzW6384xDpX6uWcXXytKD9cO00uN\\n8v9j4MT6\\n-----END PRIVATE KEY-----&quot;,&quot;teamId&quot;:&quot;QL76XYH73P&quot;,&quot;teamName&quot;:&quot;Alicja Warchał (Individual)&quot;,&quot;certP12&quot;:&quot;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
+1 -18
View File
@@ -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
View File
@@ -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;
+5
View File
@@ -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.',
+31 -14
View File
@@ -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 -1
View File
@@ -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';
+2 -1
View File
@@ -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'),
};
}
+1 -1
View File
@@ -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';
+2 -2
View File
@@ -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();
}
+7 -5
View File
@@ -1,9 +1,9 @@
import _url from 'url';
import { ConfigError, getExpoSDKVersion, ProjectConfig } from '@expo/config';
import { ExponentTools } from '@expo/xdl';
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';
@@ -79,7 +79,7 @@ export function createBuilderAction({
await setup(platform, sdkVersion);
const credentials = await prepareCredentials(cmd);
const rawJob = {
...await buildJobObject(platform, projectConfig, 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) },
};
@@ -113,6 +113,7 @@ function getExpoSDKVersionSafely(projectDir: string, projectConfig: ProjectConfi
}
const buildJobObject = async (
cmd: any,
platform: 'android' | 'ios',
projectConfig: ProjectConfig,
{ releaseChannel, buildType, buildMode, username, publicUrl, projectDir }: any,
@@ -125,11 +126,12 @@ const buildJobObject = async (
config: {
...(projectConfig.exp?.[platform]?.config || {}),
buildType,
...(platform === 'android' ? { buildMode } : {}),
releaseChannel,
...(platform === 'ios' ? { bundleIdentifier: projectConfig.exp?.ios?.bundleIdentifier } : {}),
...(platform === 'android' ? { androidPackage: projectConfig.exp?.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,
+6 -4
View File
@@ -1,6 +1,7 @@
import path from 'path';
import { AndroidShellApp, ImageUtils } from '@expo/xdl';
import { AndroidShellApp, ImageUtils } from '../xdl';
import fs from 'fs-extra';
import _ from 'lodash';
import { ANDROID_BUILD_TYPES } from 'turtle/constants';
@@ -34,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),
},
});
@@ -84,7 +85,7 @@ async function runShellAppBuilder(
logger.info({ buildPhase: 'resolve native modules' }, 'Resolving universal modules dependencies');
const packageJsonDependecies = _.get(manifest, 'dependencies');
const enabledModules = _.get(manifest, 'android.enableDangerousExperimentalLeanBuilds')
? await resolveNativeModules(workingDir, packageJsonDependecies)
? await resolveNativeModules(workingDir, sdkVersion, packageJsonDependecies)
: await resolveExplicitOptIn(workingDir, packageJsonDependecies);
try {
@@ -103,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 -1
View File
@@ -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 -1
View File
@@ -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';
+8
View File
@@ -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) {
+2 -5
View File
@@ -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 -1
View File
@@ -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';
+3 -4
View File
@@ -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 -1
View File
@@ -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 -1
View 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 -1
View File
@@ -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';
+74 -19
View File
@@ -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 -1
View File
@@ -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';
+5 -2
View File
@@ -29,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: {
@@ -100,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),
},
};
+1
View File
@@ -15,6 +15,7 @@ export interface IJob {
// android
androidPackage?: string;
buildMode?: ANDROID_BUILD_MODES;
gradleArgs?: string[];
};
credentials: {
// android
+26 -10
View File
@@ -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 },
+1
View File
@@ -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
View File
@@ -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'))
+472
View File
@@ -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);
}
}
+83
View File
@@ -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}`;
+164
View File
@@ -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 };
+2 -2
View File
@@ -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,
+56
View File
@@ -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 };
+143
View File
@@ -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 };
+103
View File
@@ -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,
};
+38
View File
@@ -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 };
+24
View File
@@ -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;
}
}
+275
View File
@@ -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 };
+42
View File
@@ -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
+76
View File
@@ -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);
}
+565
View File
@@ -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.`
);
}
}
+297
View File
@@ -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,
};
+71
View File
@@ -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 };
+343
View File
@@ -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,
};
+140
View File
@@ -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 };
}
+185
View File
@@ -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 };
+132
View File
@@ -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`;
+251
View File
@@ -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 };
+27
View File
@@ -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'));
}
}
+760
View File
@@ -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;
}
+109
View File
@@ -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 };
+586
View File
@@ -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 };
+89
View File
@@ -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);
}
}
+371
View File
@@ -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 };
+448
View File
@@ -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 };
+131
View File
@@ -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);
}
});
}
+50
View File
@@ -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;
}
+53
View File
@@ -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;
+125
View File
@@ -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 };
@@ -0,0 +1,28 @@
import StandaloneBuildFlags from './StandaloneBuildFlags';
import {
IStandaloneContextDataService,
StandaloneContext,
StandaloneContextDataType,
} from './StandaloneContext';
export class StandaloneContextService extends StandaloneContext {
public type: StandaloneContextDataType = 'service';
constructor(
public data: IStandaloneContextDataService,
public published: {
url: string;
releaseChannel: string;
},
public build: StandaloneBuildFlags,
) {
super();
}
/**
* On iOS we begin configuring standalone apps before we have any information about the
* project's manifest.
*/
public isAnonymous() {
return true;
}
}
+20
View File
@@ -0,0 +1,20 @@
import StandaloneBuildFlags from './StandaloneBuildFlags';
import {
IStandaloneContextDataUser,
StandaloneContext,
StandaloneContextDataType,
} from './StandaloneContext';
export class StandaloneContextUser extends StandaloneContext {
public type: StandaloneContextDataType = 'user';
constructor(
public data: IStandaloneContextDataUser,
public published: {
url?: string;
releaseChannel: 'default';
},
public build: StandaloneBuildFlags,
) {
super();
}
}
+17
View File
@@ -0,0 +1,17 @@
import { ncp } from 'ncp';
export function ncpAsync(
source: string,
dest: string,
options: any = {},
): Promise<void> {
return new Promise((resolve, reject) => {
ncp(source, dest, options, (err: any) => {
if (err) {
reject(err);
} else {
resolve();
}
});
});
}
+52
View File
@@ -0,0 +1,52 @@
import spawnAsync from '@expo/spawn-async';
import fs from 'fs-extra';
import path from 'path';
import semver from 'semver';
import logger from './Logger';
export default async function installPackagesAsync(
projectDir: string,
packages: string[],
options: any = {},
): Promise<void> {
let packageManager = 'npm';
if (options.packageManager) {
packageManager = options.packageManager;
} else {
const packageLockJsonExists: boolean = await fs.pathExists(
path.join(projectDir, 'package-lock.json'),
);
const yarnExists = await yarnExistsAsync();
packageManager = yarnExists && !packageLockJsonExists ? 'yarn' : 'npm';
}
if (packageManager === 'yarn') {
logger.info(`Installing dependencies using Yarn...`);
await spawnAsync('yarnpkg', ['add', '--silent', ...packages], {
cwd: projectDir,
stdio: 'inherit',
});
} else {
logger.info(`Installing dependencies using npm...`);
if (!(await fs.pathExists(path.join(projectDir, 'node_modules')))) {
await spawnAsync('npm', ['install', '--loglevel', 'error'], {
cwd: projectDir,
stdio: 'inherit',
});
}
await spawnAsync('npm', ['install', '--save', '--loglevel', 'error', ...packages], {
cwd: projectDir,
stdio: 'inherit',
});
}
}
async function yarnExistsAsync() {
try {
const version = (await spawnAsync('yarnpkg', ['--version'])).stdout.trim();
return !!semver.valid(version);
} catch (e) {
return false;
}
}
+54
View File
@@ -0,0 +1,54 @@
import * as ExponentTools from './detach/ExponentTools';
import * as IosIcons from './detach/IosIcons';
import * as IosKeychain from './detach/IosKeychain';
import * as IosPlist from './detach/IosPlist';
import LoggerDetach from './detach/Logger';
import * as PKCS12Utils from './detach/PKCS12Utils';
import * as FsCache from './FsCache';
import * as ImageUtils from './ImageUtils';
import * as Modules from './modules/Modules';
import * as ModuleVersion from './ModuleVersion';
import XDLError from './XDLError';
// tslint:disable-next-line
const AndroidShellApp: any = require("./detach/AndroidShellApp.js");
// tslint:disable-next-line
const Detach: any = require("./detach/Detach.js");
// tslint:disable-next-line
const IosCodeSigning = require("./detach/IosCodeSigning.js");
// tslint:disable-next-line
const IosIPABuilder = require("./detach/IosIPABuilder.js").default;
// tslint:disable-next-line
const IosPodsTools = require("./detach/IosPodsTools.js");
// tslint:disable-next-line
const IosShellApp = require("./detach/IosShellApp.js");
// tslint:disable-next-line
const IosWorkspace = require("./detach/IosWorkspace");
export { AndroidShellApp };
export { Detach };
export { ExponentTools };
export { FsCache };
export { ImageUtils };
export { IosIcons };
export { IosIPABuilder };
export { IosKeychain };
export { IosWorkspace };
export { IosPlist };
export { IosPodsTools };
export { IosShellApp };
export { IosCodeSigning };
export { LoggerDetach };
export { ModuleVersion };
export { Modules };
export { PKCS12Utils };
export { XDLError };
+82
View File
@@ -0,0 +1,82 @@
import semver from 'semver';
import {
expoSdkUniversalModulesConfigs,
IModuleConfig,
INativeConfig,
vendoredNativeModules,
} from './config';
type PlatformSpecificModuleConfig = IModuleConfig & INativeConfig;
type Platform = 'ios' | 'android';
const HIGHEST_KNOWN_VERSION = '10000.0.0';
export function getAllNativeModules() {
return [...expoSdkUniversalModulesConfigs, ...vendoredNativeModules].filter(
(moduleConfig) => moduleConfig.isNativeModule,
);
}
export function getAllForPlatform(
platform: Platform,
sdkVersion: string,
): PlatformSpecificModuleConfig[] {
return expoSdkUniversalModulesConfigs
.filter((moduleConfig: IModuleConfig) =>
doesVersionSatisfy(sdkVersion, moduleConfig.sdkVersions),
)
.map((moduleConfig: IModuleConfig) => ({
...moduleConfig,
...moduleConfig.config[platform],
}));
}
export function getAllNativeForExpoClientOnPlatform(
platform: Platform,
sdkVersion: string,
): PlatformSpecificModuleConfig[] {
return getAllForPlatform(platform, sdkVersion).filter(
(moduleConfig) =>
moduleConfig.includeInExpoClient && moduleConfig.isNativeModule,
);
}
export function getVersionableModulesForPlatform(
platform: Platform,
sdkVersion: string,
): PlatformSpecificModuleConfig[] {
return getAllNativeForExpoClientOnPlatform(platform, sdkVersion).filter(
(moduleConfig) => {
return moduleConfig.versionable;
},
);
}
export function getDetachableModules(
platform: Platform,
sdkVersion: string,
): PlatformSpecificModuleConfig[] {
return getAllForPlatform(platform, sdkVersion).filter(
(moduleConfig) => moduleConfig.isNativeModule && moduleConfig.detachable,
);
}
export function getPublishableModules(sdkVersion: string): IModuleConfig[] {
return expoSdkUniversalModulesConfigs.filter(
(moduleConfig: IModuleConfig) =>
!!moduleConfig.libName &&
doesVersionSatisfy(sdkVersion, moduleConfig.sdkVersions),
);
}
export function doesVersionSatisfy(
version: string,
versionRequirement: string,
): boolean {
if (version === 'UNVERSIONED') {
return semver.satisfies(HIGHEST_KNOWN_VERSION, versionRequirement);
}
return semver.satisfies(version, versionRequirement);
}
+718
View File
@@ -0,0 +1,718 @@
export interface IModuleConfig {
podName?: string;
libName: string;
sdkVersions: string;
isNativeModule: boolean;
config: {
android: INativeConfig;
ios: INativeConfig;
};
}
export interface INativeConfig {
subdirectory: string;
versionable: boolean;
detachable: boolean;
includeInExpoClient: boolean;
}
const defaultUniversalModuleConfig = {
ios: {
// subdirectory in which the module podspec is placed.
subdirectory: 'ios',
// whether when adding a new version of ABI
// the module should be versioned and released
// as eg. ABI28_0_0EXCamera
versionable: true,
// whether the module should be included in the newly created detached app.
detachable: true,
// whether the module should be included in Expo client
includeInExpoClient: true,
},
android: {
// subdirectory in which the Android project can be found.
subdirectory: 'android',
// whether when adding a new version of ABI
// the module should be versioned and released
// as eg. abi28_2_0.expo.modules.camera
versionable: true,
// whether the module should be included in the newly created detached app,
// so when releasing a new version of expoview
// as eg. host.exp.exponent:expoview:28.2.0 the module will
// be assembled and uploaded to the local maven repo
detachable: true,
// whether the module should be included in Expo client
includeInExpoClient: true,
},
};
const firebaseModuleConfig = {
ios: {
versionable: false,
detachable: false,
includeInExpoClient: false,
},
android: {
versionable: false,
detachable: false,
includeInExpoClient: false,
},
};
const expoUniversalModules = [
// native modules
{
podName: 'EXAdsAdMob',
libName: 'expo-ads-admob',
sdkVersions: '>=30.0.0',
},
{
podName: 'EXAdsFacebook',
libName: 'expo-ads-facebook',
sdkVersions: '>=33.0.0',
},
{
podName: 'EXAmplitude',
libName: 'expo-analytics-amplitude',
sdkVersions: '>=33.0.0',
},
{
podName: 'EXSegment',
libName: 'expo-analytics-segment',
sdkVersions: '>=30.0.0',
},
{
podName: 'EXAppAuth',
libName: 'expo-app-auth',
sdkVersions: '>=32.0.0',
},
{
podName: 'EXApplication',
libName: 'expo-application',
sdkVersions: '>=35.0.0',
},
{
podName: 'EXAppLoaderProvider',
libName: 'expo-app-loader-provider',
sdkVersions: '>=32.0.0',
},
{
podName: 'EXAV',
libName: 'expo-av',
sdkVersions: '>=33.0.0',
},
{
podName: 'EXBackgroundFetch',
libName: 'expo-background-fetch',
sdkVersions: '>=32.0.0',
},
{
podName: 'EXBarCodeScanner',
libName: 'expo-barcode-scanner',
sdkVersions: '>=30.0.0',
},
{
podName: 'EXBarCodeScannerInterface',
libName: 'expo-barcode-scanner-interface',
sdkVersions: '>=30.0.0 <33.0.0',
},
{
podName: 'EXBattery',
libName: 'expo-battery',
sdkVersions: '>=35.0.0',
},
{
podName: 'EXBluetooth',
libName: 'expo-bluetooth',
sdkVersions: '>=34.0.0',
config: {
ios: {
includeInExpoClient: false,
},
android: {
includeInExpoClient: false,
},
},
},
{
podName: 'EXBlur',
libName: 'expo-blur',
sdkVersions: '>=33.0.0',
config: {
android: {
versionable: false,
detachable: false,
includeInExpoClient: false,
},
},
},
{
podName: 'EXBrightness',
libName: 'expo-brightness',
sdkVersions: '>=33.0.0',
},
{
podName: 'EXCalendar',
libName: 'expo-calendar',
sdkVersions: '>=33.0.0',
},
{
podName: 'EXCamera',
libName: 'expo-camera',
sdkVersions: '>=29.0.0',
},
{
podName: 'EXCameraInterface',
libName: 'expo-camera-interface',
sdkVersions: '>=29.0.0 <33.0.0',
},
{
podName: 'EXCellular',
libName: 'expo-cellular',
sdkVersions: '>=35.0.0',
},
{
podName: 'EXConstants',
libName: 'expo-constants',
sdkVersions: '>=29.0.0',
},
{
podName: 'EXConstantsInterface',
libName: 'expo-constants-interface',
sdkVersions: '>=29.0.0 <33.0.0',
},
{
podName: 'EXContacts',
libName: 'expo-contacts',
sdkVersions: '>=30.0.0',
},
{
podName: 'EXCore',
libName: 'expo-core',
sdkVersions: '>=29.0.0 <33.0.0',
},
{
podName: 'EXCrypto',
libName: 'expo-crypto',
sdkVersions: '>=33.0.0',
},
{
podName: 'EXDevice',
libName: 'expo-device',
sdkVersions: '>=35.0.0',
},
{
podName: 'EXDocumentPicker',
libName: 'expo-document-picker',
sdkVersions: '>=33.0.0',
},
{
podName: 'EXErrors',
libName: 'expo-errors',
sdkVersions: '>=32.0.0 <33.0.0',
},
{
podName: 'EXFacebook',
libName: 'expo-facebook',
sdkVersions: '>=33.0.0',
},
{
podName: 'EXFaceDetector',
libName: 'expo-face-detector',
detachable: false,
sdkVersions: '>=29.0.0',
},
{
podName: 'EXFaceDetectorInterface',
libName: 'expo-face-detector-interface',
sdkVersions: '>=29.0.0 <33.0.0',
},
{
podName: 'EXFileSystem',
libName: 'expo-file-system',
sdkVersions: '>=29.0.0',
},
{
podName: 'EXFileSystemInterface',
libName: 'expo-file-system-interface',
sdkVersions: '>=29.0.0 <33.0.0',
},
{
podName: 'EXFirebaseAnalytics',
libName: 'expo-firebase-analytics',
sdkVersions: '>=31.0.0',
config: firebaseModuleConfig,
},
{
podName: 'EXFirebaseApp',
libName: 'expo-firebase-app',
sdkVersions: '>=31.0.0',
config: firebaseModuleConfig,
},
{
podName: 'EXFirebaseAuth',
libName: 'expo-firebase-auth',
sdkVersions: '>=31.0.0',
config: firebaseModuleConfig,
},
{
podName: 'EXFirebaseCrashlytics',
libName: 'expo-firebase-crashlytics',
sdkVersions: '>=31.0.0',
config: firebaseModuleConfig,
},
{
podName: 'EXFirebaseDatabase',
libName: 'expo-firebase-database',
sdkVersions: '>=31.0.0',
config: firebaseModuleConfig,
},
{
podName: 'EXFirebaseFirestore',
libName: 'expo-firebase-firestore',
sdkVersions: '>=31.0.0',
config: firebaseModuleConfig,
},
{
podName: 'EXFirebaseFunctions',
libName: 'expo-firebase-functions',
sdkVersions: '>=31.0.0',
config: firebaseModuleConfig,
},
{
podName: 'EXFirebaseInstanceID',
libName: 'expo-firebase-instance-id',
sdkVersions: '>=31.0.0',
config: firebaseModuleConfig,
},
{
podName: 'EXFirebaseInvites',
libName: 'expo-firebase-invites',
sdkVersions: '>=31.0.0',
config: firebaseModuleConfig,
},
{
podName: 'EXFirebaseLinks',
libName: 'expo-firebase-links',
sdkVersions: '>=31.0.0',
config: firebaseModuleConfig,
},
{
podName: 'EXFirebaseMessaging',
libName: 'expo-firebase-messaging',
sdkVersions: '>=31.0.0',
config: firebaseModuleConfig,
},
{
podName: 'EXFirebaseNotifications',
libName: 'expo-firebase-notifications',
sdkVersions: '>=31.0.0',
config: firebaseModuleConfig,
},
{
podName: 'EXFirebasePerformance',
libName: 'expo-firebase-performance',
sdkVersions: '>=31.0.0',
config: firebaseModuleConfig,
},
{
podName: 'EXFirebaseRemoteConfig',
libName: 'expo-firebase-remote-config',
sdkVersions: '>=31.0.0',
config: firebaseModuleConfig,
},
{
podName: 'EXFirebaseStorage',
libName: 'expo-firebase-storage',
sdkVersions: '>=31.0.0',
config: firebaseModuleConfig,
},
{
podName: 'EXFont',
libName: 'expo-font',
sdkVersions: '>=30.0.0',
},
{
podName: 'EXFontInterface',
libName: 'expo-font-interface',
sdkVersions: '>=30.0.0 <33.0.0',
},
{
podName: 'EXGL',
libName: 'expo-gl',
sdkVersions: '>=29.0.0',
},
{
podName: 'EXGL-CPP',
libName: 'expo-gl-cpp',
sdkVersions: '>=29.0.0',
versionable: false,
config: {
ios: {
subdirectory: 'cpp',
},
},
},
{
podName: 'EXGoogleSignIn',
libName: 'expo-google-sign-in',
sdkVersions: '>=32.0.0',
},
{
podName: 'EXHaptics',
libName: 'expo-haptics',
sdkVersions: '>=33.0.0',
},
{
podName: 'EXImageLoaderInterface',
libName: 'expo-image-loader-interface',
sdkVersions: '>=30.0.0 <33.0.0',
},
{
podName: 'EXImageManipulator',
libName: 'expo-image-manipulator',
sdkVersions: '>=33.0.0',
},
{
podName: 'EXImagePicker',
libName: 'expo-image-picker',
sdkVersions: '>=33.0.0',
},
{
podName: 'EXInAppPurchases',
libName: 'expo-in-app-purchases',
sdkVersions: '>=33.0.0',
config: {
ios: {
includeInExpoClient: false,
},
android: {
includeInExpoClient: false,
},
},
},
{
libName: 'expo-intent-launcher',
sdkVersions: '>=33.0.0',
config: {
ios: {
versionable: false,
detachable: false,
includeInExpoClient: false,
},
},
},
{
podName: 'EXKeepAwake',
libName: 'expo-keep-awake',
sdkVersions: '>=33.0.0',
},
{
podName: 'EXLinearGradient',
libName: 'expo-linear-gradient',
sdkVersions: '>=33.0.0',
},
{
podName: 'EXLocalAuthentication',
libName: 'expo-local-authentication',
sdkVersions: '>=30.0.0',
},
{
podName: 'EXLocalization',
libName: 'expo-localization',
sdkVersions: '>=31.0.0',
},
{
podName: 'EXLocation',
libName: 'expo-location',
sdkVersions: '>=30.0.0',
},
{
podName: 'EXMailComposer',
libName: 'expo-mail-composer',
sdkVersions: '>=33.0.0',
},
{
podName: 'EXMediaLibrary',
libName: 'expo-media-library',
sdkVersions: '>=30.0.0',
},
{
podName: 'EXNetwork',
libName: 'expo-network',
sdkVersions: '>=35.0.0',
},
{
podName: 'EXNotifications',
libName: 'expo-notifications',
sdkVersions: '>=38.0.0',
},
{
podName: 'EXPaymentsStripe',
libName: 'expo-payments-stripe',
sdkVersions: '>=30.0.0',
detachable: false,
config: {
ios: {
includeInExpoClient: false,
},
},
},
{
podName: 'EXPermissions',
libName: 'expo-permissions',
sdkVersions: '>=29.0.0',
},
{
podName: 'EXPermissionsInterface',
libName: 'expo-permissions-interface',
sdkVersions: '>=29.0.0 <33.0.0',
},
{
podName: 'EXPrint',
libName: 'expo-print',
sdkVersions: '>=30.0.0',
},
{
podName: 'EXRandom',
libName: 'expo-random',
sdkVersions: '>=33.0.0',
},
{
podName: 'EXReactNativeAdapter',
libName: 'expo-react-native-adapter',
sdkVersions: '>=29.0.0 <33.0.0',
},
{
podName: 'EXSecureStore',
libName: 'expo-secure-store',
sdkVersions: '>=33.0.0',
},
{
podName: 'EXSensors',
libName: 'expo-sensors',
sdkVersions: '>=29.0.0',
},
{
podName: 'EXSensorsInterface',
libName: 'expo-sensors-interface',
sdkVersions: '>=29.0.0 <33.0.0',
},
{
podName: 'EXSharing',
libName: 'expo-sharing',
sdkVersions: '>=33.0.0',
},
{
podName: 'EXSMS',
libName: 'expo-sms',
sdkVersions: '>=29.0.0',
},
{
podName: 'EXSpeech',
libName: 'expo-speech',
sdkVersions: '>=33.0.0',
},
{
podName: 'EXSQLite',
libName: 'expo-sqlite',
sdkVersions: '>=33.0.0',
},
{
podName: 'EXTaskManager',
libName: 'expo-task-manager',
sdkVersions: '>=32.0.0',
},
{
podName: 'EXTaskManagerInterface',
libName: 'expo-task-manager-interface',
sdkVersions: '>=32.0.0 <33.0.0',
config: {
android: {
versionable: false,
},
},
},
{
podName: 'EXVideoThumbnails',
libName: 'expo-video-thumbnails',
sdkVersions: '>=33.0.0',
},
{
podName: 'EXWebBrowser',
libName: 'expo-web-browser',
sdkVersions: '>=33.0.0',
},
// JS-only modules
{
libName: 'expo-asset',
sdkVersions: '>=29.0.0',
isNativeModule: false,
},
{
libName: 'expo-module-template',
sdkVersions: '>=29.0.0',
isNativeModule: false,
},
{
podName: 'UMCore',
libName: '@unimodules/core',
sdkVersions: '>=33.0.0',
},
{
podName: 'UMReactNativeAdapter',
libName: '@unimodules/react-native-adapter',
sdkVersions: '>=33.0.0',
},
{
podName: 'UMBarCodeScannerInterface',
libName: 'unimodules-barcode-scanner-interface',
sdkVersions: '>=33.0.0',
},
{
podName: 'UMCameraInterface',
libName: 'unimodules-camera-interface',
sdkVersions: '>=33.0.0',
},
{
podName: 'UMConstantsInterface',
libName: 'unimodules-constants-interface',
sdkVersions: '>=33.0.0',
},
{
podName: 'UMFaceDetectorInterface',
libName: 'unimodules-face-detector-interface',
sdkVersions: '>=33.0.0',
},
{
podName: 'UMFileSystemInterface',
libName: 'unimodules-file-system-interface',
sdkVersions: '>=33.0.0',
},
{
podName: 'UMFontInterface',
libName: 'unimodules-font-interface',
sdkVersions: '>=33.0.0',
},
{
podName: 'UMImageLoaderInterface',
libName: 'unimodules-image-loader-interface',
sdkVersions: '>=33.0.0',
},
{
podName: 'UMPermissionsInterface',
libName: 'unimodules-permissions-interface',
sdkVersions: '>=33.0.0',
},
{
podName: 'UMSensorsInterface',
libName: 'unimodules-sensors-interface',
sdkVersions: '>=33.0.0',
},
{
podName: 'UMTaskManagerInterface',
libName: 'unimodules-task-manager-interface',
sdkVersions: '>=33.0.0',
config: {
android: {
versionable: false,
},
},
},
];
export const vendoredNativeModules = [
{
libName: '@expo/vector-icons',
sdkVersions: '>=26.0.0',
isNativeModule: true,
},
{
libName: '@react-native-community/netinfo',
sdkVersions: '>=33.0.0',
isNativeModule: true,
},
{
libName: 'lottie-react-native',
sdkVersions: '>=26.0.0',
isNativeModule: true,
},
{
libName: 'react-native-branch',
sdkVersions: '>=26.0.0',
isNativeModule: true,
},
{
libName: 'react-native-gesture-handler',
sdkVersions: '>=26.0.0',
isNativeModule: true,
},
{
libName: 'react-native-maps',
sdkVersions: '>=26.0.0',
isNativeModule: true,
},
{
libName: 'react-native-reanimated',
sdkVersions: '>=28.0.0',
isNativeModule: true,
},
{
libName: 'react-native-screens',
sdkVersions: '>=30.0.0',
isNativeModule: true,
},
{
libName: 'react-native-svg',
sdkVersions: '>=26.0.0',
isNativeModule: true,
},
{
libName: 'react-native-view-shot',
sdkVersions: '>=26.0.0',
isNativeModule: true,
},
{
libName: 'react-native-webview',
sdkVersions: '>=33.0.0',
isNativeModule: true,
},
];
function defaults(
defaultConfig: INativeConfig,
...customConfigs: Array<Partial<INativeConfig> | undefined>
) {
return Object.assign({}, defaultConfig, ...customConfigs);
}
export const expoSdkUniversalModulesConfigs: IModuleConfig[] = expoUniversalModules.map(
({
config = {},
podName,
libName,
sdkVersions,
isNativeModule = true,
...params
}) => {
return {
podName,
libName,
sdkVersions,
isNativeModule,
config: {
ios: defaults(
defaultUniversalModuleConfig.ios,
params,
'ios' in config ? config.ios : undefined,
),
android: defaults(
defaultUniversalModuleConfig.android,
params,
'android' in config ? config.android : undefined,
),
},
};
},
);
+6 -1
View File
@@ -10,5 +10,10 @@
},
"rulesDirectory": [
"node_modules/tslint-eslint-rules/dist/rules"
]
],
"linterOptions": {
"exclude": [
"src/types/xcode.d.ts"
]
}
}
+1507 -7246
View File
File diff suppressed because it is too large. Load diff