Compare commits

..
Author SHA1 Message Date
Wojciech Kozyra 6f9c9fc95a Release 0.25.0 2022-03-11 13:33:17 +01:00
Wojciech Kozyra 4ea23f6433 Update @expo/xdl (disable bitcode) 2022-03-10 18:49:22 +01:00
dependabot[bot] f6199a276d Bump postcss from 7.0.14 to 7.0.39 (#378)
Bumps [postcss](https://github.com/postcss/postcss) from 7.0.14 to 7.0.39.
- [Release notes](https://github.com/postcss/postcss/releases)
- [Changelog](https://github.com/postcss/postcss/blob/7.0.39/CHANGELOG.md)
- [Commits](https://github.com/postcss/postcss/compare/7.0.14...7.0.39)

---
updated-dependencies:
- dependency-name: postcss
  dependency-type: indirect
...

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-02-21 12:14:37 +01:00
dependabot[bot] b4c9dcbc7b Bump undefsafe from 2.0.2 to 2.0.5 (#382)
Bumps [undefsafe](https://github.com/remy/undefsafe) from 2.0.2 to 2.0.5.
- [Release notes](https://github.com/remy/undefsafe/releases)
- [Commits](https://github.com/remy/undefsafe/compare/v2.0.2...v2.0.5)

---
updated-dependencies:
- dependency-name: undefsafe
  dependency-type: indirect
...

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-02-21 12:14:28 +01:00
dependabot[bot] f7433bb574 Bump pathval from 1.1.0 to 1.1.1 (#383)
Bumps [pathval](https://github.com/chaijs/pathval) from 1.1.0 to 1.1.1.
- [Release notes](https://github.com/chaijs/pathval/releases)
- [Changelog](https://github.com/chaijs/pathval/blob/master/CHANGELOG.md)
- [Commits](https://github.com/chaijs/pathval/compare/v1.1.0...v1.1.1)

---
updated-dependencies:
- dependency-name: pathval
  dependency-type: indirect
...

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-02-21 12:14:20 +01:00
dependabot[bot] e14387b439 Bump follow-redirects from 1.14.7 to 1.14.8 (#384)
Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.14.7 to 1.14.8.
- [Release notes](https://github.com/follow-redirects/follow-redirects/releases)
- [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.14.7...v1.14.8)

---
updated-dependencies:
- dependency-name: follow-redirects
  dependency-type: indirect
...

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-02-21 12:14:07 +01:00
dependabot[bot] be09542233 Bump simple-get from 3.0.3 to 3.1.1 (#387)
Bumps [simple-get](https://github.com/feross/simple-get) from 3.0.3 to 3.1.1.
- [Release notes](https://github.com/feross/simple-get/releases)
- [Commits](https://github.com/feross/simple-get/compare/v3.0.3...v3.1.1)

---
updated-dependencies:
- dependency-name: simple-get
  dependency-type: indirect
...

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-02-21 12:13:59 +01:00
dependabot[bot] acdd0e2328 Bump node-fetch from 2.6.1 to 2.6.7 (#388)
Bumps [node-fetch](https://github.com/node-fetch/node-fetch) from 2.6.1 to 2.6.7.
- [Release notes](https://github.com/node-fetch/node-fetch/releases)
- [Commits](https://github.com/node-fetch/node-fetch/compare/v2.6.1...v2.6.7)

---
updated-dependencies:
- dependency-name: node-fetch
  dependency-type: indirect
...

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-02-21 12:13:46 +01:00
dependabot[bot] f46d852f7f Bump url-parse from 1.5.3 to 1.5.7 (#386)
Bumps [url-parse](https://github.com/unshiftio/url-parse) from 1.5.3 to 1.5.7.
- [Release notes](https://github.com/unshiftio/url-parse/releases)
- [Commits](https://github.com/unshiftio/url-parse/compare/1.5.3...1.5.7)

---
updated-dependencies:
- dependency-name: url-parse
  dependency-type: indirect
...

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-02-21 12:09:06 +01:00
dependabot[bot] afa29341a7 Bump follow-redirects from 1.13.0 to 1.14.7 (#377)
Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.13.0 to 1.14.7.
- [Release notes](https://github.com/follow-redirects/follow-redirects/releases)
- [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.13.0...v1.14.7)

---
updated-dependencies:
- dependency-name: follow-redirects
  dependency-type: indirect
...

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-01-18 11:47:55 +01:00
Quinlan Jung ca842ebf56 switch cloudfront domain to classic-assets.eascdn.net (#375)
* switch cloudfront domain to classic-assets.eascdn.net

* changelog
2022-01-05 17:12:54 -08:00
Kudo Chien 6f6aec53bb Update CHANGELOG 2021-12-29 22:06:17 +08:00
Kudo Chien c126e9d5cf Release 0.24.3 2021-12-29 22:03:55 +08:00
Kudo Chien fd8e110f38 Update ios sdk 44 shell tarball with orientation lock fix 2021-12-29 20:42:16 +08:00
Kudo Chien 91545a2726 Update CHANGELOG 2021-12-17 00:33:20 +08:00
Kudo Chien 18f58cfc87 Release 0.24.2 2021-12-17 00:29:53 +08:00
Kudo Chien 5db06fa07b Update android sdk 44 shell tarball 2021-12-17 00:25:52 +08:00
Kudo Chien 4faf23de16 Update CHANGELOG 2021-12-16 01:14:35 +08:00
Kudo Chien f432a14d2f Release 0.24.1 2021-12-16 01:12:40 +08:00
Kudo Chien e957ea0b13 Update android and ios shell tarballs for sdk 44 rc (#369)
### Description

update sdk 44 tarballs mainly for react-native-gesture-handler 2.1.0 updates
2021-12-16 01:04:16 +08:00
Brent Vatne 259dc4827c Update CHANGELOG 2021-12-10 09:37:42 -08:00
Brent Vatne aaea6f80c0 Release 0.24.0 2021-12-10 09:32:37 -08:00
Brent Vatne 2ce4f9f923 Resolve modules by either unimodule.json or expo-module.config.json (#368) 2021-12-09 18:08:55 -08:00
Brent Vatne 9cd489a297 Update iOS and Android tarballs for SDK 44 beta 2021-12-09 14:18:33 -08:00
Wojciech Kozyra 06f10977aa redirect google bintray url to localhost (#361) 2021-12-09 15:28:08 +01:00
Kudo Chien 9ba1302bfa Release 0.23.6 2021-12-08 23:41:38 +08:00
Kudo Chien a166a789fa [android] Update android shell tarballs for bintray error fix (#363)
### Motivation and Context

fix android gradle build errors for dl.bintray.com and google.bintray.com return http 502.

### Description

apply https://github.com/expo/expo/pull/15349 to sdk 40-43 shell apps.
2021-12-08 23:35:00 +08:00
Wojciech Kozyra 377a8fbcf9 update aws sdk (#365)
* override region from secrets

* update aws-sdk package
2021-12-06 16:45:15 +01:00
Wojciech Kozyra e3d685e416 Revert "Bump aws-sdk from 2.407.0 to 2.814.0 (#359)"
This reverts commit 32683d082b.
2021-11-30 13:19:00 +01:00
Wojciech Kozyra 2ac41fa82d redirect google bintray url to localhost (#361) 2021-11-30 13:10:15 +01:00
dependabot[bot] 32683d082b Bump aws-sdk from 2.407.0 to 2.814.0 (#359)
Bumps [aws-sdk](https://github.com/aws/aws-sdk-js) from 2.407.0 to 2.814.0.
- [Release notes](https://github.com/aws/aws-sdk-js/releases)
- [Changelog](https://github.com/aws/aws-sdk-js/blob/master/CHANGELOG.md)
- [Commits](https://github.com/aws/aws-sdk-js/compare/v2.407.0...v2.814.0)

---
updated-dependencies:
- dependency-name: aws-sdk
  dependency-type: direct:production
...

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-11-25 13:37:11 +01:00
Kudo Chien d629cd439b update changelog 2021-11-12 11:37:03 +08:00
Kudo Chien 293e49fd00 Release 0.23.5 2021-11-12 11:34:28 +08:00
Kudo Chien 5563d16bda [ios] update SDK 43 tarball with expo-updates fix (#358) 2021-11-12 11:27:32 +08:00
Eric Samelson 6c5209e88f update changelog 2021-11-09 16:53:08 -08:00
Eric Samelson 54c73dba8e Release 0.23.4 2021-11-09 16:51:49 -08:00
Eric Samelson 81892b8c22 [android] update sdk 43 tarball (#356) 2021-11-09 16:50:48 -08:00
Eric Samelson cfd704d4b6 update changelog 2021-11-04 21:08:14 -07:00
Eric Samelson dd79348228 Release 0.23.3 2021-11-04 21:07:11 -07:00
Eric Samelson c0eaa0de4f [ios][android] update SDK 43 tarballs with expo-updates fix (#355) 2021-11-04 21:03:01 -07:00
Wojciech Kozyra fa8576a36b increase memory request for android workers 2021-11-04 11:10:08 +01:00
Cedric van Putten ac25927326 Update changelog with Android tarball bump 2021-10-29 01:18:37 +02:00
Cedric van Putten 2a23a4c97f Release 0.23.2 2021-10-29 01:17:04 +02:00
Cedric van Putten 42ed532f00 Update Android SDK 43 shell app with proguard fix (#352) 2021-10-29 01:07:11 +02:00
Wojciech Kozyra 68f9d4c7d1 move iOS builders to mac mini cluster (#350) 2021-10-27 15:02:15 +02:00
Brent Vatne c5e0206d29 Update CHANGELOG 2021-10-20 11:24:41 -07:00
Brent Vatne a8ddba34ac Release 0.23.1 2021-10-20 11:23:06 -07:00
Kudo Chien 65f2f9fc67 Update iOS SDK 43 tarball 2021-10-20 20:01:57 +08:00
Brent Vatne 5138ab57e8 Update iOS and Android SDK 43 tarballs 2021-10-19 13:53:24 -07:00
Wojciech Kozyra 5f066d14ee update node (#347) 2021-10-15 14:30:22 +02:00
dependabot[bot] b307604177 Bump tmpl from 1.0.4 to 1.0.5 (#331)
Bumps [tmpl](https://github.com/daaku/nodejs-tmpl) from 1.0.4 to 1.0.5.
- [Release notes](https://github.com/daaku/nodejs-tmpl/releases)
- [Commits](https://github.com/daaku/nodejs-tmpl/commits/v1.0.5)

---
updated-dependencies:
- dependency-name: tmpl
  dependency-type: indirect
...

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-10-15 13:11:01 +02:00
dependabot[bot] 6bdcd744df Bump url-parse from 1.5.1 to 1.5.3 (#339)
Bumps [url-parse](https://github.com/unshiftio/url-parse) from 1.5.1 to 1.5.3.
- [Release notes](https://github.com/unshiftio/url-parse/releases)
- [Commits](https://github.com/unshiftio/url-parse/compare/1.5.1...1.5.3)

---
updated-dependencies:
- dependency-name: url-parse
  dependency-type: indirect
...

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-10-15 10:38:03 +02:00
Wojciech Kozyra 9447341b84 increase memory limits (#344) 2021-10-08 14:50:30 +02:00
Brent Vatne 817653f01d Release 0.23.0 2021-10-07 11:58:46 -07:00
Brent Vatne eb0f2f5918 Update iOS SDK 43 tarball 2021-10-07 08:49:49 -07:00
Wojciech Kozyra ba4b53b41a Update Android SDK43 shell tarball (#343) 2021-10-07 16:50:48 +02:00
Wojciech Kozyra ba7b4a1968 remove workspaces from shellapp (#342) 2021-10-07 13:35:01 +02:00
Tomasz Sapeta 3cb449f7ff Update Android SDK43 tarball to fix missing expo and react-native-codegen packages 2021-10-06 17:09:28 +02:00
Brent Vatne b6ba977746 Udpate yarn.lock 2021-10-05 18:46:50 -07:00
Brent Vatne 1155e26727 Update xdl to fix Android shell app for SDK 43 2021-10-05 18:44:49 -07:00
Brent Vatne 75ead4d6a1 Add iOS beta tarball for SDK 43 2021-10-05 18:27:00 -07:00
Brent Vatne 7d5b092adc Add Android beta tarball for SDK 43 2021-10-05 17:34:36 -07:00
Wojciech Kozyra b644f15e43 Release 0.22.6 2021-10-01 15:56:13 +02:00
Wojciech Kozyra 18631f6482 Update CHANGELOG 2021-10-01 15:54:36 +02:00
Wojciech Kozyra 85e2408f0c disable ios tests 2021-10-01 15:52:23 +02:00
andy e9897054f3 update xdl to v59.1.5 (#337) 2021-10-01 15:50:00 +02:00
Wojciech Kozyra 6ae5eb1ab2 force rebuild (switch from catalina to big sur) (#334) 2021-09-30 14:21:49 +02:00
Cedric van Putten a32799a584 Update tarball to fix local assets on SDK 42 (#332)
* Update tarball to fix local assets on SDK 42

* Update changelog
2021-09-21 22:15:51 +02:00
Cedric van Putten 6f86b4ac57 Update CHANGELOG.md 2021-09-09 23:35:07 +02:00
Cedric van Putten 9f07dbedb5 Release 0.22.5 2021-09-09 23:21:59 +02:00
Cedric van Putten 3e579ceeba Update changelog with Android tarball bumps 2021-09-09 23:05:59 +02:00
Cedric van Putten a2913c28bf Update tarballs for Android location workaround (#330)
* Update tarball for Android SDK 39

* Update tarball for Android SDK 41

* Update tarball for Android SDK 42

* Update tarball for Android SDK 40
2021-09-09 21:46:44 +02:00
dependabot[bot] a0f9810320 Bump tar from 4.4.15 to 4.4.18 (#329)
Bumps [tar](https://github.com/npm/node-tar) from 4.4.15 to 4.4.18.
- [Release notes](https://github.com/npm/node-tar/releases)
- [Changelog](https://github.com/npm/node-tar/blob/main/CHANGELOG.md)
- [Commits](https://github.com/npm/node-tar/compare/v4.4.15...v4.4.18)

---
updated-dependencies:
- dependency-name: tar
  dependency-type: direct:production
...

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-09-09 16:16:05 +02:00
Will Schurman 74a3739b6e [logger] Fix s3 logger stream close crash (#326) 2021-08-31 10:16:33 -07:00
dependabot[bot] 2c36c89ed4 Bump tar from 4.4.8 to 4.4.15 (#321)
Bumps [tar](https://github.com/npm/node-tar) from 4.4.8 to 4.4.15.
- [Release notes](https://github.com/npm/node-tar/releases)
- [Changelog](https://github.com/npm/node-tar/blob/main/CHANGELOG.md)
- [Commits](https://github.com/npm/node-tar/compare/v4.4.8...v4.4.15)

---
updated-dependencies:
- dependency-name: tar
  dependency-type: direct:production
...

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-08-31 16:48:11 +02:00
dependabot[bot] 099afed72c Bump path-parse from 1.0.6 to 1.0.7 (#323)
Bumps [path-parse](https://github.com/jbgutierrez/path-parse) from 1.0.6 to 1.0.7.
- [Release notes](https://github.com/jbgutierrez/path-parse/releases)
- [Commits](https://github.com/jbgutierrez/path-parse/commits/v1.0.7)

---
updated-dependencies:
- dependency-name: path-parse
  dependency-type: indirect
...

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-08-31 14:28:59 +02:00
Wojciech Kozyra 36d1d75544 update to node-forge 0.10.0 (#328) 2021-08-31 12:03:04 +02:00
Juwan Wheatley 70eca74b9c update expo.io links to expo.dev/exp.host (#325) 2021-08-24 14:14:53 -04:00
Brent Vatne 01137526c4 Update CHANGELOG 2021-07-27 17:59:56 -07:00
Brent Vatne 4972744a87 Release 0.22.4 2021-07-27 17:58:47 -07:00
Brent Vatne cbd11f2f4e Update tarballs for SDK 42 2021-07-27 15:20:16 -07:00
Brent Vatne fdd76085c1 Release 0.22.3 2021-07-02 10:44:08 -07:00
Brent Vatne daedefbca8 Update tarball for SDK 42 2021-07-02 10:43:43 -07:00
Brent Vatne e7da1a0193 Update changelog 2021-06-29 19:42:37 -07:00
Brent Vatne 85cc6d1b9f Release 0.22.2 2021-06-29 19:41:48 -07:00
Brent Vatne 07679dfbdc [ios] Update tarball again for SDK 42 2021-06-29 19:41:15 -07:00
Brent Vatne 070485dd09 Update changelog 2021-06-29 14:06:50 -07:00
Brent Vatne bf3e1feac9 Release 0.22.1 2021-06-29 14:05:58 -07:00
Brent Vatne fcba536698 [ios][android] Update tarballs for SDK 42 2021-06-29 14:03:10 -07:00
dependabot[bot] e77f05ea1d Bump set-getter from 0.1.0 to 0.1.1 (#320)
Bumps [set-getter](https://github.com/doowb/set-getter) from 0.1.0 to 0.1.1.
- [Release notes](https://github.com/doowb/set-getter/releases)
- [Commits](https://github.com/doowb/set-getter/commits/0.1.1)

---
updated-dependencies:
- dependency-name: set-getter
  dependency-type: indirect
...

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-06-25 15:10:02 +02:00
Brent Vatne 819be0056c Update changelog 2021-06-22 19:34:11 -07:00
Brent Vatne 0a3faaf975 Release 0.22.0 2021-06-22 19:33:09 -07:00
Brent Vatne c38b1b4089 Update SDK 42 beta tarballs 2021-06-22 17:50:13 -07:00
Brent Vatne 5c5de300a6 Add iOS and Android beta tarballs for SDK 42 2021-06-21 18:37:40 -07:00
Wojciech Kozyra 6efa267a1f Update CHANGELOG 2021-06-15 14:14:41 +02:00
Wojciech Kozyra 81b7d8d911 Release 0.21.9 2021-06-15 14:13:17 +02:00
Wojciech Kozyra 95a48861e0 update sharp and skip NDK install on M1 (#319) 2021-06-15 14:12:02 +02:00
dependabot[bot] 2bb14a67b6 Bump url-parse from 1.4.4 to 1.5.1 (#310)
Bumps [url-parse](https://github.com/unshiftio/url-parse) from 1.4.4 to 1.5.1.
- [Release notes](https://github.com/unshiftio/url-parse/releases)
- [Commits](https://github.com/unshiftio/url-parse/compare/1.4.4...1.5.1)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-06-07 17:15:37 +02:00
dependabot[bot] 5a1f48335f Bump lodash from 4.17.19 to 4.17.21 (#311)
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.19 to 4.17.21.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.19...4.17.21)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-06-07 17:15:28 +02:00
dependabot[bot] 2dd89d2c08 Bump hosted-git-info from 2.7.1 to 2.8.9 (#312)
Bumps [hosted-git-info](https://github.com/npm/hosted-git-info) from 2.7.1 to 2.8.9.
- [Release notes](https://github.com/npm/hosted-git-info/releases)
- [Changelog](https://github.com/npm/hosted-git-info/blob/v2.8.9/CHANGELOG.md)
- [Commits](https://github.com/npm/hosted-git-info/compare/v2.7.1...v2.8.9)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-06-07 17:15:18 +02:00
dependabot[bot] 5f9d98b1dc Bump dns-packet from 1.3.1 to 1.3.4 (#318)
Bumps [dns-packet](https://github.com/mafintosh/dns-packet) from 1.3.1 to 1.3.4.
- [Release notes](https://github.com/mafintosh/dns-packet/releases)
- [Changelog](https://github.com/mafintosh/dns-packet/blob/master/CHANGELOG.md)
- [Commits](https://github.com/mafintosh/dns-packet/compare/v1.3.1...v1.3.4)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-06-07 17:14:53 +02:00
cruzach 02833e211d update changelog 2021-05-25 10:36:38 -04:00
cruzach ba4f2cc05a Release 0.21.8 2021-05-25 10:35:28 -04:00
cruzach 7f9b7a17d4 update android tarball for SDK 41 2021-05-24 16:49:34 -04:00
cruzach 821e648d67 update changelog 2021-05-21 10:32:45 -04:00
cruzach 54a6e1910c Release 0.21.7 2021-05-21 10:31:15 -04:00
cruzach 688f970a1a Update android and ios tarballs for SDK 41 2021-05-20 17:04:38 -04:00
Brent Vatne 9c666d90b4 Update CHANGELOG 2021-04-22 14:24:09 -07:00
Brent Vatne 4f351a047f Release 0.21.6 2021-04-22 14:21:25 -07:00
Brent Vatne 7caa13bdda Update Android SDK41 tarball. 2021-04-22 14:19:41 -07:00
Tomasz Sapeta 1f266f7b72 Update iOS SDK41 shell tarball 2021-04-22 15:25:11 +02:00
Wojciech Kozyra edfa2dbb94 increase android worker memory and cpu limits - (12GB, 3CPU) 2021-04-20 11:24:19 +02:00
Wojciech Kozyra 1d5075d898 increase android worker memory and cpu limits (#307) 2021-04-20 10:56:01 +02:00
dependabot[bot] 64147f0cf7 Bump ssri from 6.0.1 to 6.0.2 (#308)
Bumps [ssri](https://github.com/npm/ssri) from 6.0.1 to 6.0.2.
- [Release notes](https://github.com/npm/ssri/releases)
- [Changelog](https://github.com/npm/ssri/blob/v6.0.2/CHANGELOG.md)
- [Commits](https://github.com/npm/ssri/compare/v6.0.1...v6.0.2)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-04-20 10:10:58 +02:00
Brent Vatne 688a961513 Update CHANGELOG 2021-04-15 17:44:15 -07:00
Brent Vatne 19ed88150a Release 0.21.5 2021-04-15 15:27:48 -07:00
Brent Vatne e988fb282e Update iOS client and Android SDK 41 tarballs 2021-04-15 15:26:57 -07:00
Brent Vatne 16103cebf9 Release 0.21.4 2021-04-13 18:47:35 -07:00
Brent Vatne be14dca375 Update iOS client tarball 2021-04-13 18:47:10 -07:00
Brent Vatne bf24c86399 Update CHANGELOG 2021-04-13 15:53:51 -07:00
Brent Vatne 7fb1e57a40 Release 0.21.3 2021-04-13 15:52:59 -07:00
Brent Vatne d6fcc6d881 Update SDK 41 taralls for iOS and Android 2021-04-13 15:51:38 -07:00
cruzach 525ddf6f04 update changelog 2021-04-07 18:20:29 -04:00
cruzach f0416d131d Release 0.21.2 2021-04-07 18:18:31 -04:00
cruzach 2fdeea268c Update Android SDK 41 beta tarball 2021-04-07 17:37:52 -04:00
Brent Vatne a11ed9198c Update CHANGELOG 2021-04-01 15:46:52 -07:00
Brent Vatne 2da016e4ce Release 0.21.1 2021-04-01 15:45:11 -07:00
Brent Vatne 8278520474 Update SDK 41 beta tarball 2021-04-01 14:54:34 -07:00
dependabot[bot] 6ab9d99d7c Bump y18n from 4.0.0 to 4.0.1 (#304)
Bumps [y18n](https://github.com/yargs/y18n) from 4.0.0 to 4.0.1.
- [Release notes](https://github.com/yargs/y18n/releases)
- [Changelog](https://github.com/yargs/y18n/blob/master/CHANGELOG.md)
- [Commits](https://github.com/yargs/y18n/commits)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-03-30 15:54:20 +02:00
dependabot[bot] c8ad765d5d Bump react-dev-utils from 11.0.1 to 11.0.4 (#299)
Bumps [react-dev-utils](https://github.com/facebook/create-react-app/tree/HEAD/packages/react-dev-utils) from 11.0.1 to 11.0.4.
- [Release notes](https://github.com/facebook/create-react-app/releases)
- [Changelog](https://github.com/facebook/create-react-app/blob/master/CHANGELOG-1.x.md)
- [Commits](https://github.com/facebook/create-react-app/commits/HEAD/packages/react-dev-utils)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-03-30 15:52:51 +02:00
Brent Vatne 1834f323ce Update CHANGELOG 2021-03-29 18:05:39 -07:00
Brent Vatne 8e261a8827 Release 0.21.0 2021-03-29 18:04:47 -07:00
Brent Vatne d0033c2df1 Bump xdl 2021-03-29 16:29:56 -07:00
Brent Vatne 63b7879608 Add beta tarballs for SDK 41 2021-03-29 15:32:08 -07:00
Brent Vatne cf57eceb6e Add entry for 0.20.7 to CHANGELOG 2021-03-29 15:31:45 -07:00
dependabot[bot] 5058cbe8e5 Bump elliptic from 6.5.3 to 6.5.4 (#297)
Bumps [elliptic](https://github.com/indutny/elliptic) from 6.5.3 to 6.5.4.
- [Release notes](https://github.com/indutny/elliptic/releases)
- [Commits](https://github.com/indutny/elliptic/compare/v6.5.3...v6.5.4)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-03-29 13:00:01 +02:00
dependabot[bot] 4804a3453c Bump ini from 1.3.5 to 1.3.8 (#293)
Bumps [ini](https://github.com/isaacs/ini) from 1.3.5 to 1.3.8.
- [Release notes](https://github.com/isaacs/ini/releases)
- [Commits](https://github.com/isaacs/ini/compare/v1.3.5...v1.3.8)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-03-29 12:59:08 +02:00
80 changed files with 7813 additions and 8737 deletions

No files matched your search

-2
View File
@@ -307,8 +307,6 @@ workflows:
tests:
jobs:
- unit_tests
- ios_smoke_tests
- android_smoke_tests
deploy:
jobs:
- android_staging_approve_deploy:
+6 -5
View File
@@ -1,10 +1,9 @@
---
name: "\U0001F41B Bug Report"
about: You want to report a reproducible bug or regression in turtle-cli.
title: ''
labels: 'bug'
assignees: ''
title: ""
labels: "bug"
assignees: ""
---
<!-- Thanks for helping expo! Before you submit your issue, please make sure you followed our checklist and check the appropriate boxes by putting an x in the [ ]: [x] -->
@@ -13,14 +12,16 @@ assignees: ''
- [ ] Updated turtle-cli to the latest version (`yarn global add turtle-cli` or `npm install --global turtle-cli`).
- [ ] I read the [Contribution Guidelines](https://github.com/expo/turtle/blob/master/CONTRIBUTING.md).
- [ ] I read [docs.expo.io](https://docs.expo.io) (Managed Workflow > Distributing Your App).
- [ ] I read [docs.expo.dev](https://docs.expo.dev) (Managed Workflow > Distributing Your App).
- [ ] I searched for [existing GitHub issues](https://github.com/expo/turtle/issues).
- [ ] **I enclosed the complete output when running turtle, including the stack trace and command used.**
### Issue Description
<!-- Please include what's happening, expected behavior, and any relevant code samples -->
##### Command executed
<!-- The command you executed on the command line that resulted in an error -->
##### Complete output when running turtle, including the stack trace and command used
+6 -5
View File
@@ -1,21 +1,22 @@
---
name: "\U00002753 Questions and discussions"
about: You have a question about turtle-cli or want to discuss some aspects of turtle-cli.
title: ''
labels: 'question'
assignees: ''
title: ""
labels: "question"
assignees: ""
---
### Question Checklist
- [ ] I read the [Contribution Guidelines](https://github.com/expo/turtle/blob/master/CONTRIBUTING.md).
- [ ] I read [docs.expo.io](https://docs.expo.io) (Managed Workflow > Distributing Your App).
- [ ] I read [docs.expo.dev](https://docs.expo.dev) (Managed Workflow > Distributing Your App).
- [ ] I searched for [existing GitHub issues](https://github.com/expo/turtle/issues).
### Question Subject
<!-- What command do you have a question about? -->
<!-- Is this a question about documentation? -->
#### Question Description
<!-- Please include expected behavior and any relevant code samples with your question if possible -->
+124 -1
View File
@@ -7,7 +7,130 @@ 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.
- Swap out Cloudfront CDN for `classic-assets.eascdn.net`. [#375](https://github.com/expo/turtle/pull/375)
# [0.24.3] - 2021-12-29
- Update ios sdk 44 shell tarball with [orientation lock fix](https://github.com/expo/expo/pull/15715)
# [0.24.2] - 2021-12-16
- Update Android shell tarball for SDK 44.
# [0.24.1] - 2021-12-15
- Update iOS and Android tarballs for SDK 44 RC. [#369](https://github.com/expo/turtle/pull/369).
# [0.24.0] - 2021-12-10
- Update iOS and Android tarballs for SDK 44 beta.
- Add detection for Expo modules using `expo-module.config.json`, the successor to `unimodule.json`. [#368](https://github.com/expo/turtle/pull/368).
- Redirect google bintray url to localhost. [#361](https://github.com/expo/turtle/pull/361)
# [0.23.6] - 2021-12-08
- Update android shell tarballs for bintray error fix. [#363](https://github.com/expo/turtle/pull/363)
- Update AWS SDK. [#365](https://github.com/expo/turtle/pull/365)
# [0.23.5] - 2021-11-12
- Update iOS SDK 43 tarball with expo-updates fix for [#14967](https://github.com/expo/expo/issues/14967)
# [0.23.4] - 2021-11-09
- Update Android SDK 43 tarball with expo-updates fix for [#15112](https://github.com/expo/expo/issues/15112)
# [0.23.3] - 2021-11-04
- Update iOS and Android SDK 43 tarballs with expo-updates fixes for [#14930](https://github.com/expo/expo/issues/14930)
# [0.23.2] - 2021-10-29
- Update Android SDK 43 shell app with proguard fix
# [0.23.1] - 2021-10-20
- Add iOS and Android tarballs for SDK 43
# [0.23.0] - 2021-10-07
- Add iOS and Android beta tarballs for SDK 43
- Bump xdl to fix Android shell app for SDK 43
# [0.22.6] - 2021-10-01
- add `ACCESS_MEDIA_LOCATION` to blacklist for android permissions
- disable automatic versioning in Xcode 13
# [0.22.5] - 2021-09-09
- Fix s3 logger stream close crash.
- Update Android tarballs SDK 42, 41, 40, and 39 to include location workaround.
- Update Android tarball to include a local assets fix on SDK 42.
# [0.22.4] - 2021-07-27
- Update iOS and Android tarballs for SDK 42.
# [0.22.3] - 2021-07-02
- Update Android tarball for SDK 42.
# [0.22.1] - 2021-06-29
- Update iOS and Android beta tarballs for SDK 42.
# [0.22.0] - 2021-06-22
- Add iOS and Android beta tarballs for SDK 42.
# [0.21.9] - 2021-06-15
- Update `sharp` package, fixes support for M1 macs.
# [0.21.8] - 2021-05-25
- Update Android SDK41 tarball.
# [0.21.7] - 2021-05-21
- Update iOS SDK41 tarball.
- Update Android SDK41 tarball.
# [0.21.6] - 2021-04-22
- Update iOS SDK41 tarball.
- Update Android SDK41 tarball.
# [0.21.5] - 2021-04-15
- Revert iOS client tarball for SDK 41, use the SDK 40 tarball. No longer supporting this feature in SDK 41+.
- Update Android SDK 41 tarball.
# [0.21.4] - 2021-04-13
- Update iOS client tarball for SDK 41.
# [0.21.3] - 2021-04-13
- Update SDK 41 tarball for iOS and Android.
# [0.21.2] - 2021-04-07
- Update SDK 41 beta tarball for Android.
# [0.21.1] - 2021-04-01
- Update SDK 41 beta tarball for iOS.
# [0.21.0] - 2021-03-29
- Add SDK 41 beta tarballs.
- Bump @expo/xdl to 59.1.0.
# [0.20.7] - 2021-01-26
- Remove Go suffix from android app name
# [0.20.6] - 2021-01-25
+1 -1
View File
@@ -4,4 +4,4 @@ The Expo team is committed to fostering an open and welcoming environment.
**Our Community Guidelines can be found here:**
https://expo.io/guidelines
https://expo.dev/guidelines
+9 -2
View File
@@ -1,30 +1,37 @@
# Contributing to Turtle CLI
## How to contribute
You can contribute to Turtle CLI in various ways, including:
- [Reporting bugs or issues](https://github.com/expo/turtle/issues/new) on GitHub. Please make sure to fill in all the details suggested by the template to make sure the issue can be addressed as quickly as possible.
- [Submitting improvements to the documentation](https://github.com/expo/expo/tree/master/docs). Updates, enhancements, new guides, spelling fixes…
- Helping other people on [Expo Forums](https://forums.expo.io/) and on [Expo Developers Slack](http://slack.expo.io).
- Helping other people on [Expo Forums](https://forums.expo.dev/) and on [Expo Developers Discord](http://chat.expo.dev).
- Looking at existing [issues](https://github.com/expo/turtle/issues) and adding more information, particularly helping to reproduce the issues.
- [Submitting a pull request](#submitting-a-pull-request) with a bug fix or an improvement.
## Turtle CLI repository
### Codebase
This repository contains the codebase for the `turtle-cli` NPM package. The same codebase is used to run standalone app builders on the Expo servers.
### Branches
The `master` branch of the repository should be kept releasable at any time. This way we can continuously release fixes and improvements without costly managing of different branches and issues will be noticed and fixed quickly. This also ensures other contributors can check out the latest version from GitHub and work on it with minimal disruption from other features in progress.
## Submitting a pull request
To submit a pull request:
1. Fork the [repository](https://github.com/expo/turtle) and create a feature branch. (Existing contributors can create feature branches without forking. Prefix the branch name with `@your-github-username/`.)
2. Write the description of your pull request. Make sure to include a test plan and test your changes.
3. Make sure all tests pass on CircleCI.
4. Wait for a review and adjust the code if necessary.
## Publishing a release
- Make sure you have set `GITHUB_TOKEN` and `CIRCLE_API_USER_TOKEN` environment variables in `.envrc.local` file. If you don't have the file, run `cp .envrc.local.example .envrc.local` and edit the file.
- To release a new version of `turtle-cli`, run `yarn release` command.
- If you wish to release a new *beta* version of `turtle-cli`, run `yarn release:beta` instead.
- If you wish to release a new _beta_ version of `turtle-cli`, run `yarn release:beta` instead.
- Update the [changelog](CHANGELOG.md).
+5 -4
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.5.0
FROM gcr.io/exponentjs/turtle-android-base:0.6.0
ADD . /app/turtle
@@ -11,12 +11,13 @@ RUN ln -s /app/turtle/workingdir /app/workingdir && \
for SDK_VERSION in `ls /app/workingdir/android/`; do \
echo "preparing $SDK_VERSION shell app" && \
cd /app/workingdir/android/$SDK_VERSION && \
mv packages non-workspace-packages && \
mv package.json copy_package.json && \
jq '.workspaces.packages = ["packages/expo", "packages/expo-modules-autolinking"]' copy_package.json > package.json && \
rm -f copy_package.json && \
# Keep in sync with src/bin/setup/android/index.ts#_installNodeModules
# --prod requires --ignore-scripts (otherwise
# expo-yarn-workspaces errors as missing)
yarn install --ignore-scripts --prod && \
mv non-workspace-packages packages ; \
yarn install --ignore-scripts --prod ; \
done
ENV NODE_ENV "production"
+8 -8
View File
@@ -6,8 +6,8 @@
[![NPM](https://img.shields.io/npm/v/turtle-cli/latest.svg)](https://npmjs.com/package/turtle-cli)
[![License](https://img.shields.io/badge/license-MIT-green.svg?style=flat)](https://github.com/expo/turtle/blob/master/LICENSE)
[![Expo Developers Slack](https://img.shields.io/badge/Expo%20Developers-e01563.svg?logo=slack)](http://slack.expo.io)
[![Expo Forums](https://img.shields.io/badge/Expo%20Forums-blue.svg)](https://forums.expo.io/)
[![Expo Developers Discord](https://img.shields.io/discord/695411232856997968)](http://chat.expo.dev)
[![Expo Forums](https://img.shields.io/badge/Expo%20Forums-blue.svg)](https://forums.expo.dev/)
<hr />
@@ -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/distribution/turtle-cli/)
[Installation instructions and documentation here.](https://docs.expo.dev/distribution/turtle-cli/)
## Usage example
@@ -29,13 +29,13 @@ See the [expo/turtle-cli-example](https://github.com/expo/turtle-cli-example) re
## Do you need help?
- Make sure you've read [the Expo Docs](https://docs.expo.io/), especially [Building Standalone Apps on Your CI
](https://docs.expo.io/versions/latest/distribution/turtle-cli) and [Building Standalone Apps](https://docs.expo.io/versions/latest/distribution/building-standalone-apps) guides.
- Make sure you've read [the Expo Docs](https://docs.expo.dev/), especially [Building Standalone Apps on Your CI
](https://docs.expo.dev/versions/latest/distribution/turtle-cli) and [Building Standalone Apps](https://docs.expo.dev/versions/latest/distribution/building-standalone-apps) guides.
- Search for existing GitHub issues [in this repository](https://github.com/expo/turtle/issues) and in [expo/expo](https://github.com/expo/expo/issues) repository.
- Ask for help in **one** of the following ways:
* create [a GitHub issue](https://github.com/expo/turtle/issues/new).
* create a new topic on [Expo Forums](https://forums.expo.io/).
* ask a question on [Expo Developers Slack](http://slack.expo.io).
- create [a GitHub issue](https://github.com/expo/turtle/issues/new).
- create a new topic on the [Expo Forums](https://forums.expo.dev/).
- connect with the community on the [Expo Developers Discord](http://chat.expo.dev).
## License
+1 -1
View File
@@ -32,7 +32,7 @@ RUN dpkg --add-architecture i386 && \
apt-get clean
ENV NPM_CONFIG_LOGLEVEL warn
ENV NODE_VERSION 12.16.1
ENV NODE_VERSION 12.22.7
RUN curl -SLO "https://nodejs.org/dist/v$NODE_VERSION/node-v$NODE_VERSION-linux-x64.tar.xz" \
&& tar -xJf "node-v$NODE_VERSION-linux-x64.tar.xz" -C /usr/local --strip-components=1 \
+5 -11
View File
@@ -64,10 +64,7 @@ spec:
name: turtle-js-config
key: aws_secret_access_key
- name: AWS_SQS_REGION
valueFrom:
secretKeyRef:
name: turtle-js-config
key: aws_sqs_region
value: us-west-2
- name: AWS_SQS_IOS_QUEUE_URL
valueFrom:
secretKeyRef:
@@ -144,15 +141,12 @@ spec:
- name: DISABLE_DEX_MAX_HEAP
value: "true"
- name: JAVA_OPTS
value: '-Xms2048m -Xmx8192m -XX:+HeapDumpOnOutOfMemoryError -XX:+UseG1GC'
value: '-Xms2048m -Xmx17g -XX:+HeapDumpOnOutOfMemoryError -XX:+UseG1GC'
- name: GRADLE_OPTS
value: '-Dorg.gradle.parallel=true -Dorg.gradle.configureondemand=true -Dorg.gradle.daemon=false -Dorg.gradle.jvmargs="-Xms2048m -Xmx8192m -XX:+HeapDumpOnOutOfMemoryError -XX:+UseG1GC"'
value: '-Dorg.gradle.parallel=true -Dorg.gradle.configureondemand=true -Dorg.gradle.daemon=false -Dorg.gradle.jvmargs="-Xms2048m -Xmx17g -XX:+HeapDumpOnOutOfMemoryError -XX:+UseG1GC"'
- name: REDIS_CA_CERTIFICATE
value: '/var/run/config/redislabs-tls/redislabs_ca.pem'
resources:
requests:
memory: "8192Mi"
cpu: "2000m"
limits:
memory: "8192Mi"
cpu: "2000m"
memory: "17Gi" # 17.8 GiB is max value reachable on current nodes
cpu: "5000m"
+11 -36
View File
@@ -1,6 +1,6 @@
{
"name": "turtle-cli",
"version": "0.20.7",
"version": "0.25.0",
"description": "The command-line tool for building standalone Expo apps",
"preferGlobal": true,
"scripts": {
@@ -57,7 +57,7 @@
"engines": {
"node": ">=10"
},
"author": "Expo <support@expo.io>",
"author": "Expo <support@expo.dev>",
"license": "MIT",
"bugs": {
"url": "https://github.com/expo/turtle/issues"
@@ -67,30 +67,26 @@
"registry": "https://registry.npmjs.org/"
},
"devDependencies": {
"@expo/babel-preset-cli": "^0.2.18",
"@expo/babel-preset-cli": "^0.1.3",
"@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/node-forge": "^0.10.0",
"@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",
"@types/semver": "^6.0.0",
"@types/sharp": "^0.17.8",
"@types/sharp": "0.28.3",
"@types/tar": "^4.0.0",
"@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": "^26.1.0",
@@ -109,21 +105,12 @@
"typescript": "^3.9.5"
},
"dependencies": {
"@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",
"@expo/spawn-async": "^1.4.2",
"@expo/xdl": "^59.2.0",
"@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",
"aws-sdk": "^2.1043.0",
"bunyan": "^1.8.12",
"bunyan-debug-stream": "^1.0.8",
"bunyan-sentry-stream": "^1.2.1",
@@ -135,36 +122,24 @@
"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",
"sharp": "0.28.3",
"source-map-support": "^0.5.6",
"tar": "^4.4.6",
"url-join": "4.0.0",
"uuid": "^3.2.1",
"which": "^1.3.1",
"xcode": "^2.1.0",
"xmldom": "0.1.27"
"which": "^1.3.1"
},
"optionalDependencies": {
"@expo/traveling-fastlane-darwin": "1.11.4"
},
"volta": {
"node": "12.16.1",
"node": "12.22.7",
"yarn": "1.22.0"
}
}
+5 -1
View File
@@ -19,4 +19,8 @@ yes | $SDKMANAGER \
"extras;google;m2repository" \
"extras;google;google_play_services" | grep -v '='; [[ ${PIPESTATUS[1]} = 0 ]]
yes | $SDKMANAGER "ndk;17.2.4988734" | grep -v '='; [[ ${PIPESTATUS[1]} = 0 ]]
if [[ "$(uname -s)" == "Darwin" ]] && [[ "$(uname -m)" == "arm64" ]]; then
echo "Skipping NDK install on M1 macOS"
else
yes | $SDKMANAGER "ndk;17.2.4988734" | grep -v '='; [[ ${PIPESTATUS[1]} = 0 ]]
fi
+1 -1
View File
@@ -18,7 +18,7 @@ export AWS_S3_BUCKET="exp-shell-app-assets"
export AWS_S3_REGION="us-west-1"
export AWS_CLOUDWATCH_REGION="us-west-1"
export API_PROTOCOL="http"
export API_HOSTNAME="staging.expo.io"
export API_HOSTNAME="staging.exp.host"
export API_PORT="443"
export REDIS_URL="noop"
export REDIS_CONFIG_URL="noop"
+1 -1
View File
@@ -14,7 +14,7 @@ export AWS_S3_BUCKET="exp-shell-app-assets"
export AWS_S3_REGION="us-west-1"
export AWS_CLOUDWATCH_REGION="us-west-1"
export API_PROTOCOL="https"
export API_HOSTNAME="staging.expo.io"
export API_HOSTNAME="staging.exp.host"
export API_PORT="443"
export REDIS_URL="noop"
export REDIS_CONFIG_URL="noop"
+1 -1
View File
@@ -1 +1 @@
s3://exp-artifacts/android-shell-builder-d292dcd0cc277ab665957244a8b15b9981debd29.tar.gz
s3://exp-artifacts/android-shell-builder-3fe600781956ce0823143d747a51afec5f9afe4f.tar.gz
+1 -1
View File
@@ -1 +1 @@
s3://exp-artifacts/android-shell-builder-277393d97877f56ba3639ab153c69f109d248436.tar.gz
s3://exp-artifacts/android-shell-builder-da0e85b79332cb8e93178aefcd90f7450e6e923e.tar.gz
+1
View File
@@ -0,0 +1 @@
s3://exp-artifacts/android-shell-builder-a7bb029adbc5636ac738759d70d80cc0f90dd757.tar.gz
+1
View File
@@ -0,0 +1 @@
s3://exp-artifacts/android-shell-builder-8725597cdf885f3d34cbbd7f74c6d1e0b9c31c93.tar.gz
+1
View File
@@ -0,0 +1 @@
s3://exp-artifacts/android-shell-builder-fe8aed82dd482644ed51d0dc45ae82c4c40a55c1.tar.gz
+1
View File
@@ -0,0 +1 @@
s3://exp-artifacts/android-shell-builder-6c4b7564ec3fe33100e80f5c9da065a678499b17.tar.gz
+1
View File
@@ -0,0 +1 @@
s3://exp-artifacts/ios-shell-builder-sdk-latest-2b9ea15e13f36c120c257039cf76e14573760570.tar.gz
+1
View File
@@ -0,0 +1 @@
s3://exp-artifacts/ios-shell-builder-sdk-latest-1bdbf48b083ef475c294128074a4149b276062ff.tar.gz
+1
View File
@@ -0,0 +1 @@
s3://exp-artifacts/ios-shell-builder-sdk-latest-2726b3298180818910646966bd82b7efc8936b5a.tar.gz
+1
View File
@@ -0,0 +1 @@
s3://exp-artifacts/ios-shell-builder-sdk-latest-088e13d4a4a77f952256bf2c361cbd3c20160821.tar.gz
+3 -3
View File
@@ -83,7 +83,7 @@
],
"description": "A very interesting project.",
"icon": "./assets/images/icon.png",
"iconUrl": "https://d1wp6m56sqw74a.cloudfront.net/~assets/fa6577fecc0a7838f15a254577639984",
"iconUrl": "https://classic-assets.eascdn.net/~assets/fa6577fecc0a7838f15a254577639984",
"ios": {
"bundleIdentifier": "com.niech.to.zadziala",
"supportsTablet": true
@@ -101,7 +101,7 @@
"splash": {
"backgroundColor": "#ffffff",
"image": "./assets/images/splash.png",
"imageUrl": "https://d1wp6m56sqw74a.cloudfront.net/~assets/43ec0dcbe5a156bf9e650bb8c15e7af6",
"imageUrl": "https://classic-assets.eascdn.net/~assets/43ec0dcbe5a156bf9e650bb8c15e7af6",
"resizeMode": "contain"
},
"updates": {
@@ -112,7 +112,7 @@
"revisionId": "1.0.0-r.zV1W89wn23",
"publishedTime": "2018-10-16T08:44:05.362Z",
"commitTime": "2018-10-16T08:44:05.403Z",
"bundleUrl": "https://d1wp6m56sqw74a.cloudfront.net/%40admin%2FtestApp%2F1.0.0%2Fb8678e2be7cd5bb9e240db0dd3c691c0-29.0.0-ios.js",
"bundleUrl": "https://classic-assets.eascdn.net/%40admin%2FtestApp%2F1.0.0%2Fb8678e2be7cd5bb9e240db0dd3c691c0-29.0.0-ios.js",
"releaseChannel": "default",
"hostUri": "exp.host/@admin/testApp"
},
+14 -31
View File
@@ -1,27 +1,19 @@
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();
@@ -41,24 +33,19 @@ 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.`,
@@ -72,11 +59,7 @@ 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';
+9 -4
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';
@@ -42,7 +42,7 @@ export default async function ensureAndroidSDKIsPresent() {
}
}
return {
envVars: _createEnvVars(androidSdkDir),
envVars: await _createEnvVars(androidSdkDir),
path: _createPaths(androidSdkDir),
};
}
@@ -63,11 +63,16 @@ async function _configureSdk(androidSdkDir: string) {
});
}
function _createEnvVars(androidSdkDir: string) {
async function _createEnvVars(androidSdkDir: string) {
const ndkPath = path.join(androidSdkDir, 'ndk/17.2.4988734');
const ndkPathExists = await fs.pathExists(ndkPath);
if (!ndkPathExists) {
l.warn('Skipping NDK installation');
}
return {
ANDROID_HOME: androidSdkDir,
ANDROID_SDK: androidSdkDir,
ANDROID_NDK_HOME: path.join(androidSdkDir, 'ndk/17.2.4988734'),
...(ndkPathExists ? { ANDROID_NDK_HOME: ndkPath } : {}),
};
}
+1 -1
View File
@@ -3,7 +3,7 @@ import trim from 'lodash/trim';
import path from 'path';
import semver from 'semver';
import { ExponentTools } from '../../xdl';
import { ExponentTools } from '@expo/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(String(sdkMajorVersion));
const filePath = formatters.formatShellAppTarballUriPath(sdkMajorVersion);
const data = await fs.readFile(filePath, 'utf8');
return data.trim();
}
+1 -1
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';
+9 -2
View File
@@ -1,7 +1,6 @@
import path from 'path';
import { AndroidShellApp, ImageUtils } from '../xdl';
import { AndroidShellApp, ImageUtils } from '@expo/xdl';
import fs from 'fs-extra';
import _ from 'lodash';
import { ANDROID_BUILD_TYPES } from 'turtle/constants';
@@ -18,7 +17,15 @@ import config from 'turtle/config';
import { IAndroidCredentials, IJob, IJobResult } from 'turtle/job';
import logger from 'turtle/logger';
// temporary solution until we can rebuild shellapps
const blockGoogleBintrayAsync = _.once(async () => {
if (config.builder.mode === 'online') {
await fs.appendFile('/etc/hosts', '\n127.0.0.1\tdl.bintray.com\n127.0.0.1\tgoogle.bintray.com\n');
}
});
export default async function buildAndroid(jobData: IJob): Promise<IJobResult> {
await blockGoogleBintrayAsync();
await ensureCanBuildSdkVersion(jobData);
const credentials = await getOrCreateCredentials(jobData);
const outputFilePath = await runShellAppBuilder(jobData, credentials);
+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';
+1 -1
View File
@@ -1,6 +1,6 @@
import path from 'path';
import { ExponentTools } from '../../../xdl';
import { ExponentTools } from '@expo/xdl';
import { IShellAppDirectoryConfig } from 'turtle/builders/utils/workingdir';
import config from 'turtle/config';
+1 -1
View File
@@ -1,8 +1,8 @@
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 BuildError, { BuildErrorReason } from 'turtle/builders/BuildError';
import { IJob } from 'turtle/job';
+1 -1
View File
@@ -1,4 +1,4 @@
import { IosKeychain } from '../../../xdl';
import { IosKeychain } from '@expo/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 '../../../xdl';
import { ExponentTools } from '@expo/xdl';
import { IShellAppDirectoryConfig } from 'turtle/builders/utils/workingdir';
import config from 'turtle/config';
+3 -1
View File
@@ -209,7 +209,9 @@ async function getPackage(
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 isUnimodule =
(await fs.pathExists(path.join(pkgPath, 'unimodule.json'))) ||
(await fs.pathExists(path.join(pkgPath, 'expo-module.config.json')));
const hasPackageJson = await fs.pathExists(
path.join(pkgPath, 'package.json'),
);
+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';
+2 -2
View File
@@ -29,13 +29,13 @@ class Logger {
jobID: job.id,
experienceName: job.experienceName,
});
const s3Url = await streams.s3.stream.init(job);
const s3Url = await streams.s3?.stream.init(job);
return s3Url;
}
public async cleanup() {
this.currentLogger = this.parentLogger;
await streams.s3.stream.cleanup();
await streams.s3?.stream.cleanup();
}
public trace(...all: any[]) {
+9 -7
View File
@@ -1,20 +1,22 @@
import pickBy from 'lodash/pickBy';
import { pickBy } from 'lodash';
import createGCloudStream from 'turtle/logger/streams/gcloud';
import createOfflineStream from 'turtle/logger/streams/offline';
import createS3Stream from 'turtle/logger/streams/s3';
import createS3Stream, { S3Stream } from 'turtle/logger/streams/s3';
import createSentryStream from 'turtle/logger/streams/sentry';
import { isOffline } from 'turtle/turtleContext';
interface IStream {
stream: any;
interface IStream<T> {
stream: T;
type?: string;
level?: string;
reemitErrorEvents?: boolean;
}
interface IStreamMap {
[key: string]: IStream;
stdout: IStream<any>;
gcloud?: IStream<any>;
s3?: IStream<S3Stream>;
sentry?: IStream<any>;
}
function prepareStreams(): IStreamMap {
@@ -29,7 +31,7 @@ function prepareStreams(): IStreamMap {
s3: createS3Stream(),
sentry: createSentryStream(),
};
return pickBy(allStreams) as IStreamMap;
return pickBy(allStreams) as any as IStreamMap;
}
}
+7 -3
View File
@@ -18,7 +18,7 @@ export default function create() {
};
}
class S3Stream extends EventEmitter {
export class S3Stream extends EventEmitter {
private s3Url: string | null = null;
private lastFlush: number | null = null;
private filePath: string | null = null;
@@ -44,7 +44,11 @@ class S3Stream extends EventEmitter {
public async cleanup() {
await this.flush(true);
await fs.close(this.fileHandle as number);
if (this.fileHandle) {
await fs.close(this.fileHandle);
}
this.fileHandle = null;
this.waitingOnPromise = false;
}
@@ -64,7 +68,7 @@ class S3Stream extends EventEmitter {
return;
}
fs
.write(this.fileHandle as number, `${JSON.stringify(rec)}\n`)
.write(this.fileHandle, `${JSON.stringify(rec)}\n`)
.then(() => {
this.flush();
});
+8 -19
View File
@@ -1,25 +1,16 @@
import { LoggerDetach } from './xdl';
import { LoggerDetach } from '@expo/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 { 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';
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()) {
@@ -36,10 +27,9 @@ 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]();
@@ -50,9 +40,7 @@ async function main() {
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) {
@@ -71,6 +59,7 @@ async function main() {
async function electLeaderAndSynchronize() {
if (config.env === 'test') {
return;
}
const l = new Leader({
key: config.leader.redisKey,
-143
View File
@@ -1,143 +0,0 @@
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
@@ -1,103 +0,0 @@
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
@@ -1,38 +0,0 @@
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
@@ -1,24 +0,0 @@
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
@@ -1,275 +0,0 @@
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
@@ -1,42 +0,0 @@
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
@@ -1,76 +0,0 @@
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
@@ -1,565 +0,0 @@
// 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
@@ -1,297 +0,0 @@
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
@@ -1,71 +0,0 @@
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
@@ -1,343 +0,0 @@
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
@@ -1,140 +0,0 @@
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
@@ -1,185 +0,0 @@
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
@@ -1,132 +0,0 @@
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
@@ -1,251 +0,0 @@
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
@@ -1,27 +0,0 @@
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
@@ -1,760 +0,0 @@
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
@@ -1,109 +0,0 @@
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
@@ -1,586 +0,0 @@
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
@@ -1,89 +0,0 @@
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
@@ -1,371 +0,0 @@
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
@@ -1,448 +0,0 @@
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
@@ -1,131 +0,0 @@
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
@@ -1,50 +0,0 @@
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
@@ -1,53 +0,0 @@
/*
* 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
@@ -1,125 +0,0 @@
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 };
@@ -1,28 +0,0 @@
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
@@ -1,20 +0,0 @@
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
@@ -1,17 +0,0 @@
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
@@ -1,52 +0,0 @@
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
@@ -1,54 +0,0 @@
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
@@ -1,82 +0,0 @@
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
@@ -1,718 +0,0 @@
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,
),
},
};
},
);
+7543 -469
View File
File diff suppressed because it is too large. Load diff