mirror of
https://github.com/expo/eas-cli.git
synced 2026-11-13 20:28:08 -10:00
Compare commits
268
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e9088dbb93 | ||
|
|
f08392888d | ||
|
|
fbcc04eb62 | ||
|
|
42940b7c4c | ||
|
|
d29a88eafd | ||
|
|
67087e9471 | ||
|
|
96dc437217 | ||
|
|
f68cbb48d7 | ||
|
|
50f1269a9c | ||
|
|
3bc8a8e3e1 | ||
|
|
510de5de4f | ||
|
|
0deeb469d6 | ||
|
|
9b91bc37c9 | ||
|
|
952b275c90 | ||
|
|
9dd75445c3 | ||
|
|
da6703706a | ||
|
|
673d8c3712 | ||
|
|
c6463af3b9 | ||
|
|
f94dc6bd59 | ||
|
|
51926d13db | ||
|
|
789cf62bd6 | ||
|
|
a476d44066 | ||
|
|
d8a87a6195 | ||
|
|
846aacd320 | ||
|
|
079aa5dc66 | ||
|
|
fa8980759f | ||
|
|
790085ea9e | ||
|
|
9c7ac49f3e | ||
|
|
fa244d6410 | ||
|
|
db8a3770f8 | ||
|
|
be16b0032f | ||
|
|
3ef14b5eb7 | ||
|
|
c777b25e92 | ||
|
|
15b690c857 | ||
|
|
4d04463714 | ||
|
|
1c31702653 | ||
|
|
b053e0926a | ||
|
|
6eeb15bdce | ||
|
|
279eb1595c | ||
|
|
256b02d3e9 | ||
|
|
9f4fef7626 | ||
|
|
8cc324e11e | ||
|
|
f33b4c131d | ||
|
|
e3eb3ad5ea | ||
|
|
1292465c57 | ||
|
|
00d2022680 | ||
|
|
5aaaf1b224 | ||
|
|
aaf14b8bab | ||
|
|
65413d1640 | ||
|
|
71ee9469aa | ||
|
|
7223a19fc8 | ||
|
|
3f4e826315 | ||
|
|
ea7cc6b1ea | ||
|
|
b5281d64d1 | ||
|
|
e51fe11aca | ||
|
|
16767f3feb | ||
|
|
c42b1a004c | ||
|
|
8a4bf6f068 | ||
|
|
03e1c18d34 | ||
|
|
2bf8822b1d | ||
|
|
460f98da76 | ||
|
|
3a7182bb82 | ||
|
|
f963dc711b | ||
|
|
a059852bd9 | ||
|
|
33e0b0901c | ||
|
|
8ba877ab81 | ||
|
|
9f6e387888 | ||
|
|
c10691c339 | ||
|
|
155b65c8fc | ||
|
|
cdbc95295c | ||
|
|
414ccb92c0 | ||
|
|
aabf519a9d | ||
|
|
88d9627286 | ||
|
|
59ce6d0b65 | ||
|
|
8e64a9c73e | ||
|
|
4476c87962 | ||
|
|
18ecd0844a | ||
|
|
cc3e7cdbe4 | ||
|
|
aa7fecb9fe | ||
|
|
c73a3e3a1e | ||
|
|
37ca2ebd55 | ||
|
|
dead11964e | ||
|
|
b66d31d7d7 | ||
|
|
ed8170047b | ||
|
|
0e4056ba82 | ||
|
|
6182ea5a1d | ||
|
|
c9e7b649ad | ||
|
|
7a6b796242 | ||
|
|
804eb9a326 | ||
|
|
8da14f0101 | ||
|
|
dc62d22816 | ||
|
|
258c625611 | ||
|
|
e84340774d | ||
|
|
fbc23f7a7d | ||
|
|
97255ec90d | ||
|
|
cfccc87b82 | ||
|
|
09b4f8367e | ||
|
|
45481f9758 | ||
|
|
1119008317 | ||
|
|
123a0c1b7e | ||
|
|
2f21fa62b1 | ||
|
|
8ad449c6b7 | ||
|
|
fe8ec50806 | ||
|
|
86cefeb259 | ||
|
|
034e0a1e7f | ||
|
|
a6b776915f | ||
|
|
4ae52d0e3d | ||
|
|
f62720ef5a | ||
|
|
af09aaedb3 | ||
|
|
184616e874 | ||
|
|
6bb09c4e99 | ||
|
|
81add86c61 | ||
|
|
4bb1c784d8 | ||
|
|
322082c28b | ||
|
|
5db1d21aa5 | ||
|
|
0db5585b20 | ||
|
|
10ec54345a | ||
|
|
27b4710264 | ||
|
|
4132e92da5 | ||
|
|
264340a717 | ||
|
|
1d09c2c97d | ||
|
|
72cff1df1a | ||
|
|
6912e453ef | ||
|
|
839cbac47b | ||
|
|
5ba16dbc67 | ||
|
|
896b456dd3 | ||
|
|
f2c0202f06 | ||
|
|
497bac1cbf | ||
|
|
5b9734b60b | ||
|
|
3ac70d8977 | ||
|
|
32adc788d0 | ||
|
|
e465ed57cb | ||
|
|
df819fc232 | ||
|
|
539e201972 | ||
|
|
c14b3e179b | ||
|
|
f093197dea | ||
|
|
13834039a8 | ||
|
|
4d3023f7f1 | ||
|
|
eb7d3b5ecd | ||
|
|
8c721759bd | ||
|
|
c6a741bb8c | ||
|
|
af5b76fe9f | ||
|
|
aa428b7571 | ||
|
|
ba68f40922 | ||
|
|
1380b023ba | ||
|
|
adfc79abd4 | ||
|
|
92e6ecd03c | ||
|
|
05546f0bfd | ||
|
|
6c5c99b5d1 | ||
|
|
f27ea08839 | ||
|
|
15f47aea81 | ||
|
|
fd23f031b6 | ||
|
|
a74c434718 | ||
|
|
29950a7629 | ||
|
|
0fd33a95f5 | ||
|
|
843ed0d0aa | ||
|
|
bf2cde45c6 | ||
|
|
ecab803cfa | ||
|
|
570ff126a0 | ||
|
|
e24b21bd1e | ||
|
|
20da7f8778 | ||
|
|
14e002274f | ||
|
|
3b8dc6b7aa | ||
|
|
445a48a8dd | ||
|
|
f6436319ed | ||
|
|
bd1e32622d | ||
|
|
74dcb40caa | ||
|
|
f094146dcd | ||
|
|
3784745382 | ||
|
|
72be7e80bf | ||
|
|
cf1b8cb5eb | ||
|
|
b6df8d3a57 | ||
|
|
b96a262cd4 | ||
|
|
3df84f28aa | ||
|
|
554f411fae | ||
|
|
e6ce9837e6 | ||
|
|
30d8a0ce99 | ||
|
|
a9c5907230 | ||
|
|
ad0f94aca9 | ||
|
|
bf66b79126 | ||
|
|
dcc5d62800 | ||
|
|
878ed19f0c | ||
|
|
0e17e415b7 | ||
|
|
d4c5a4e1e4 | ||
|
|
3776c95f00 | ||
|
|
7804a00597 | ||
|
|
5c27574df2 | ||
|
|
f2ab223f0e | ||
|
|
38e64cbd9a | ||
|
|
2cd7194ef6 | ||
|
|
6c33c8a656 | ||
|
|
92643026f3 | ||
|
|
b8a056b43e | ||
|
|
1ba5cb07ce | ||
|
|
8e96143b59 | ||
|
|
52512c333d | ||
|
|
576c798666 | ||
|
|
356d6267ec | ||
|
|
a88897b40e | ||
|
|
8431167a13 | ||
|
|
d87f8d5750 | ||
|
|
1a261cf043 | ||
|
|
9daa933990 | ||
|
|
72f3ba817f | ||
|
|
e8ffdf25f7 | ||
|
|
196abc9a9e | ||
|
|
037529689a | ||
|
|
2502b843f8 | ||
|
|
012e75f8ee | ||
|
|
5ada92430b | ||
|
|
bf27ec5238 | ||
|
|
8f18b35fe8 | ||
|
|
e82b8966b0 | ||
|
|
fad19a1e03 | ||
|
|
553d648e86 | ||
|
|
5588a7343d | ||
|
|
8ec3c52be8 | ||
|
|
e29d7916b8 | ||
|
|
b9840556fc | ||
|
|
3cfb6725e7 | ||
|
|
de794c37f4 | ||
|
|
c9a0747762 | ||
|
|
80dfcdbe29 | ||
|
|
6b05dd6226 | ||
|
|
f1f189b3f4 | ||
|
|
e9f03f7941 | ||
|
|
683a778b15 | ||
|
|
43ccaa6129 | ||
|
|
d2c4608f3f | ||
|
|
d6b2caa4d6 | ||
|
|
78cd4bf86a | ||
|
|
ce8e098a86 | ||
|
|
94494489de | ||
|
|
50ad49cf96 | ||
|
|
6a378b369b | ||
|
|
3f41cdd58e | ||
|
|
ec38cdb154 | ||
|
|
2b89aed724 | ||
|
|
b9334ab7a3 | ||
|
|
0f717c0f22 | ||
|
|
537f95aa7b | ||
|
|
1dee08def7 | ||
|
|
15f6b04daa | ||
|
|
c0f27b8201 | ||
|
|
430181893f | ||
|
|
6636f76cff | ||
|
|
d7b47d080c | ||
|
|
046d8d75bb | ||
|
|
2d1f913648 | ||
|
|
b0522484af | ||
|
|
28ee6b95c9 | ||
|
|
67d85c2025 | ||
|
|
d213040dc4 | ||
|
|
dda4fe45be | ||
|
|
d5c0b1b3d7 | ||
|
|
f968654ffc | ||
|
|
a66753cde9 | ||
|
|
0c85c68948 | ||
|
|
78770912ab | ||
|
|
727dc105f8 | ||
|
|
e63de21702 | ||
|
|
e3bbdd519e | ||
|
|
25cc555189 | ||
|
|
be73d3a337 | ||
|
|
ecb6458a28 | ||
|
|
475d90ea23 | ||
|
|
59171230d4 | ||
|
|
b8b16139bf |
No files matched your search
+2
-1
@@ -1,8 +1,9 @@
|
||||
**/build/**
|
||||
**/dist/**
|
||||
!packages/eas-cli/src/commands/build/**
|
||||
!packages/eas-cli/src/build/**
|
||||
|
||||
packages/eas-cli/src/graphql/generated.ts
|
||||
|
||||
node_modules
|
||||
ts-declarations
|
||||
ts-declarations
|
||||
+8
-1
@@ -15,7 +15,14 @@ module.exports = {
|
||||
'import/no-cycle': 'error',
|
||||
'import/no-extraneous-dependencies': [
|
||||
'error',
|
||||
{ devDependencies: ['**/__tests__/**/*', '**/__mocks__/**/*'] },
|
||||
{
|
||||
devDependencies: [
|
||||
'**/jest/**/*',
|
||||
'**/__tests__/**/*',
|
||||
'**/__mocks__/**/*',
|
||||
'**/__integration-tests__/**/*',
|
||||
],
|
||||
},
|
||||
],
|
||||
'import/no-relative-packages': 'error',
|
||||
'no-restricted-imports': [
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
name: setup-mise
|
||||
description: Install tools via mise
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- uses: jdx/mise-action@146a28175021df8ca24f8ee1828cc2a60f980bd5 # v3.5.1
|
||||
with:
|
||||
install: true
|
||||
cache: true
|
||||
experimental: true
|
||||
@@ -3,7 +3,7 @@ commentConfiguration:
|
||||
epilogue: 'Generated by [CodeMention](https://github.com/expo/expo/actions/workflows/codemention.yaml)'
|
||||
rules:
|
||||
- patterns: ['**/*']
|
||||
mentions: ['khamilowicz', 'sjchmiela']
|
||||
mentions: ['douglowder']
|
||||
- patterns: ['packages/eas-cli/src/update/**']
|
||||
mentions: ['wschurman', 'quinlanj']
|
||||
- patterns: ['packages/eas-cli/src/commands/update/**']
|
||||
@@ -22,3 +22,9 @@ rules:
|
||||
mentions: ['kitten', 'kadikraman']
|
||||
- patterns: ['packages/eas-cli/src/commands/deploy/**']
|
||||
mentions: ['kitten', 'kadikraman']
|
||||
- patterns: ['packages/eas-cli/src/commands/build/**']
|
||||
mentions: ['sjchmiela']
|
||||
- patterns: ['packages/eas-cli/src/build/**']
|
||||
mentions: ['sjchmiela']
|
||||
- patterns: ['packages/eas-cli/src/commands/workflow/**']
|
||||
mentions: ['sjchmiela']
|
||||
@@ -0,0 +1,17 @@
|
||||
name: build-android
|
||||
on:
|
||||
workflow_dispatch: {}
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build Android
|
||||
type: build
|
||||
runs_on: linux-large
|
||||
params:
|
||||
platform: android
|
||||
profile: system-test
|
||||
custom_job:
|
||||
name: Custom Job
|
||||
steps:
|
||||
- name: Echo
|
||||
run: echo "This is a custom job step"
|
||||
@@ -0,0 +1,16 @@
|
||||
name: setup-gcloud
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Auth gcloud
|
||||
uses: google-github-actions/auth@v2
|
||||
with:
|
||||
workload_identity_provider: 'projects/321830142373/locations/global/workloadIdentityPools/github/providers/expo'
|
||||
project_id: exponentjs
|
||||
|
||||
- name: Setup gcloud
|
||||
uses: google-github-actions/setup-gcloud@e427ad8a34f8676edf47cf7d7925499adf3eb74f # v2
|
||||
with:
|
||||
version: 548.0.0
|
||||
project_id: exponentjs
|
||||
@@ -0,0 +1,205 @@
|
||||
name: build-and-deploy-worker
|
||||
# Assumes main branch always specifies an environment (staging/production).
|
||||
# If environment is empty: PR testing build (no upload, no deploy)
|
||||
# If environment is set: Build (unless cached on main), upload, and deploy
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
environment:
|
||||
description: '"staging", "production" or empty'
|
||||
type: string
|
||||
required: true
|
||||
workflow_call:
|
||||
inputs:
|
||||
environment:
|
||||
description: '"staging", "production" or empty'
|
||||
type: string
|
||||
required: true
|
||||
|
||||
jobs:
|
||||
build-and-deploy:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
id-token: write
|
||||
steps:
|
||||
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4
|
||||
|
||||
- uses: ./.github/actions/setup-mise
|
||||
|
||||
- uses: ./.github/internal-actions/setup-gcloud
|
||||
|
||||
- name: Check if worker already exists
|
||||
id: check_worker_exists
|
||||
# Check if tarballs for this SHA already exist
|
||||
# If they do, we can skip building regardless of branch
|
||||
if: inputs.environment != ''
|
||||
run: |
|
||||
for platform in android ios; do
|
||||
GCP_TARBALL="worker-$platform-sha1-$GITHUB_SHA.tar.gz"
|
||||
|
||||
if ! gsutil stat gs://eas-build-worker-tarballs/$GCP_TARBALL; then
|
||||
echo "worker_exists=false" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
done
|
||||
|
||||
echo "worker_exists=true" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Start EAS Workflow to build worker tarballs
|
||||
id: eas_workflow
|
||||
# Build if: PR testing (no env), or deploying but tarballs don't exist
|
||||
if: inputs.environment == '' || steps.check_worker_exists.outputs.worker_exists == 'false'
|
||||
working-directory: packages/worker
|
||||
env:
|
||||
EXPO_TOKEN: ${{ secrets.EXPO_DEV_EXPO_GITHUB_ROBOT_ACCESS_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
# Only upload artifacts from EAS if GHA will upload them to GCS
|
||||
# This avoids unnecessary artifact uploads for PR builds
|
||||
SHOULD_UPLOAD_ARTIFACT="${{ inputs.environment != '' && steps.check_worker_exists.outputs.worker_exists == 'false' }}"
|
||||
|
||||
# Run workflow and capture output (includes run ID)
|
||||
WORKFLOW_OUTPUT=$(npx -y eas-cli@latest workflow:run build-worker.yml \
|
||||
--ref ${{ github.sha }} \
|
||||
--input upload_artifact=$SHOULD_UPLOAD_ARTIFACT \
|
||||
--non-interactive \
|
||||
--no-wait \
|
||||
--json)
|
||||
|
||||
WORKFLOW_RUN_ID=$(echo "$WORKFLOW_OUTPUT" | jq -r '.id')
|
||||
if [ -z "$WORKFLOW_RUN_ID" ] || [ "$WORKFLOW_RUN_ID" = "null" ]; then
|
||||
echo "Error: Could not determine workflow run ID"
|
||||
echo "Workflow output:"
|
||||
echo "$WORKFLOW_OUTPUT" | jq .
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "workflow_run_id=$WORKFLOW_RUN_ID" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Wait for EAS workflow and capture artifacts
|
||||
id: eas_artifacts
|
||||
if: inputs.environment == '' || steps.check_worker_exists.outputs.worker_exists == 'false'
|
||||
working-directory: packages/worker
|
||||
env:
|
||||
EXPO_TOKEN: ${{ secrets.EXPO_DEV_EXPO_GITHUB_ROBOT_ACCESS_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
WORKFLOW_RUN_ID="${{ steps.eas_workflow.outputs.workflow_run_id }}"
|
||||
if [ -z "$WORKFLOW_RUN_ID" ]; then
|
||||
echo "Error: Missing workflow run ID"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
SHOULD_UPLOAD_ARTIFACT="${{ inputs.environment != '' && steps.check_worker_exists.outputs.worker_exists == 'false' }}"
|
||||
|
||||
while true; do
|
||||
VIEW_OUTPUT=$(npx -y eas-cli@latest workflow:view "$WORKFLOW_RUN_ID" \
|
||||
--non-interactive \
|
||||
--json)
|
||||
STATUS=$(echo "$VIEW_OUTPUT" | jq -r '.status')
|
||||
|
||||
case "$STATUS" in
|
||||
SUCCESS)
|
||||
break
|
||||
;;
|
||||
FAILURE)
|
||||
echo "EAS workflow failed."
|
||||
echo "$VIEW_OUTPUT" | jq .
|
||||
exit 1
|
||||
;;
|
||||
CANCELED)
|
||||
echo "EAS workflow canceled."
|
||||
echo "$VIEW_OUTPUT" | jq .
|
||||
exit 1
|
||||
;;
|
||||
ACTION_REQUIRED)
|
||||
echo "EAS workflow requires action."
|
||||
echo "$VIEW_OUTPUT" | jq .
|
||||
exit 1
|
||||
;;
|
||||
NEW|IN_PROGRESS)
|
||||
sleep 7
|
||||
;;
|
||||
*)
|
||||
echo "Unexpected workflow status: $STATUS"
|
||||
echo "$VIEW_OUTPUT" | jq .
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Skip artifact extraction if we didn't request uploads
|
||||
if [ "$SHOULD_UPLOAD_ARTIFACT" == "false" ]; then
|
||||
echo "Skipping artifact download - upload was disabled for this run"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Extract artifact download URLs from workflow output
|
||||
# Artifacts are in jobs[].artifacts[] for custom jobs
|
||||
DOWNLOAD_IOS_URL=$(echo "$VIEW_OUTPUT" | jq -r '
|
||||
.jobs[]
|
||||
| .artifacts[]?
|
||||
| select(.name == "worker-ios-tarball")
|
||||
| .downloadUrl
|
||||
' | head -1)
|
||||
|
||||
DOWNLOAD_ANDROID_URL=$(echo "$VIEW_OUTPUT" | jq -r '
|
||||
.jobs[]
|
||||
| .artifacts[]?
|
||||
| select(.name == "worker-android-tarball")
|
||||
| .downloadUrl
|
||||
' | head -1)
|
||||
|
||||
if [ -z "$DOWNLOAD_IOS_URL" ] || [ "$DOWNLOAD_IOS_URL" = "null" ]; then
|
||||
echo "Error: Could not find iOS artifact download URL"
|
||||
echo "$VIEW_OUTPUT" | jq .
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "$DOWNLOAD_ANDROID_URL" ] || [ "$DOWNLOAD_ANDROID_URL" = "null" ]; then
|
||||
echo "Error: Could not find Android artifact download URL"
|
||||
echo "$VIEW_OUTPUT" | jq .
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Mask the download URLs
|
||||
echo "::add-mask::$DOWNLOAD_IOS_URL"
|
||||
echo "::add-mask::$DOWNLOAD_ANDROID_URL"
|
||||
echo "download_ios_url=$DOWNLOAD_IOS_URL" >> "$GITHUB_OUTPUT"
|
||||
echo "download_android_url=$DOWNLOAD_ANDROID_URL" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Cancel EAS workflow on GitHub cancellation
|
||||
if: cancelled()
|
||||
working-directory: packages/worker
|
||||
env:
|
||||
EXPO_TOKEN: ${{ secrets.EXPO_DEV_EXPO_GITHUB_ROBOT_ACCESS_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
WORKFLOW_RUN_ID="${{ steps.eas_workflow.outputs.workflow_run_id }}"
|
||||
if [ -n "$WORKFLOW_RUN_ID" ]; then
|
||||
npx -y eas-cli@latest workflow:cancel "$WORKFLOW_RUN_ID" --non-interactive
|
||||
fi
|
||||
|
||||
- name: Upload worker tarballs to GCP
|
||||
# Upload when deploying if we built something new
|
||||
# Don't upload PR builds (no environment) or when using cached tarballs
|
||||
if: inputs.environment != '' && steps.check_worker_exists.outputs.worker_exists == 'false'
|
||||
run: |
|
||||
IOS_GCP_TARBALL="worker-ios-sha1-$GITHUB_SHA.tar.gz"
|
||||
ANDROID_GCP_TARBALL="worker-android-sha1-$GITHUB_SHA.tar.gz"
|
||||
curl -L "${{ steps.eas_artifacts.outputs.download_ios_url }}" | gsutil cp - gs://eas-build-worker-tarballs/$IOS_GCP_TARBALL
|
||||
curl -L "${{ steps.eas_artifacts.outputs.download_android_url }}" | gsutil cp - gs://eas-build-worker-tarballs/$ANDROID_GCP_TARBALL
|
||||
|
||||
- name: Promote worker to ${{ inputs.environment }}
|
||||
if: inputs.environment != ''
|
||||
run: |
|
||||
for platform in android ios; do
|
||||
GCP_TARBALL="worker-$platform-sha1-$GITHUB_SHA.tar.gz"
|
||||
WORKER_STAGING_TARBALL="worker-$platform-${{ inputs.environment }}.tar.gz"
|
||||
|
||||
gsutil cp gs://eas-build-worker-tarballs/$GCP_TARBALL gs://eas-build-worker-tarballs/$WORKER_STAGING_TARBALL
|
||||
done
|
||||
@@ -0,0 +1,64 @@
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
staging_only:
|
||||
type: boolean
|
||||
required: true
|
||||
default: false
|
||||
description: "Only move the latest-eas-build-staging tag, not the latest-eas-build tag"
|
||||
version:
|
||||
type: string
|
||||
required: true
|
||||
description: "Version number to move the tag to"
|
||||
dry_run:
|
||||
type: boolean
|
||||
required: true
|
||||
default: true
|
||||
description: "Run the workflow in dry run mode, without making any NPM changes"
|
||||
|
||||
name: Move NPM tags used by EAS builds and workflows
|
||||
|
||||
jobs:
|
||||
move-tag:
|
||||
name: Move NPM tags used by EAS builds and workflows
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.EXPO_BOT_PAT }}
|
||||
NPM_ACCESS_TOKEN: ${{ secrets.NPM_ACCESS_TOKEN }}
|
||||
INPUT_VERSION: ${{ github.event.inputs.version }}
|
||||
INPUT_DRY_RUN: ${{ github.event.inputs.dry_run }}
|
||||
INPUT_STAGING_ONLY: ${{ github.event.inputs.staging_only }}
|
||||
steps:
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
registry-url: "https://registry.npmjs.org/"
|
||||
scope: "expo"
|
||||
node-version: 22
|
||||
- name: Resolve version (fails if the version does not exist)
|
||||
run: |
|
||||
set -o pipefail
|
||||
npm pack --dry-run eas-cli@$INPUT_VERSION
|
||||
- name: Add latest-eas-build-staging tag
|
||||
run: |
|
||||
cmd="npm dist-tag add eas-cli@$INPUT_VERSION latest-eas-build-staging"
|
||||
if [[ "$INPUT_DRY_RUN" == "true" ]]; then
|
||||
echo "Running in dry run mode, skipping version check"
|
||||
echo "cmd = $cmd"
|
||||
exit 0
|
||||
fi
|
||||
echo //registry.npmjs.org/:_authToken=$NPM_ACCESS_TOKEN > ~/.npmrc
|
||||
$cmd
|
||||
- name: Add latest-eas-build tag
|
||||
run: |
|
||||
cmd="npm dist-tag add eas-cli@$INPUT_VERSION latest-eas-build"
|
||||
if [[ "$INPUT_DRY_RUN" == "true" ]]; then
|
||||
echo "Running in dry run mode, skipping version check"
|
||||
echo "cmd = $cmd"
|
||||
exit 0
|
||||
fi
|
||||
if [[ "$INPUT_STAGING_ONLY" == "true" ]]; then
|
||||
echo "Only moving the staging tag, skipping the production tag"
|
||||
exit 0
|
||||
fi
|
||||
echo //registry.npmjs.org/:_authToken=$NPM_ACCESS_TOKEN > ~/.npmrc
|
||||
$cmd
|
||||
@@ -9,6 +9,8 @@ jobs:
|
||||
release:
|
||||
name: Create a release
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
outputs:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
steps:
|
||||
@@ -18,18 +20,23 @@ jobs:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
tag_name: ${{ github.ref }}
|
||||
release_name: ${{ github.ref }}
|
||||
draft: true
|
||||
build-linux:
|
||||
name: Build for Linux
|
||||
runs-on: ubuntu-latest
|
||||
needs: release
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 20
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ./.github/actions/setup-mise
|
||||
- run: yarn install --frozen-lockfile --check-files
|
||||
- run: yarn build
|
||||
- name: Ensure GraphQL schema and generated code is up-to-date
|
||||
working-directory: ${{ github.workspace }}/packages/eas-cli
|
||||
run: |
|
||||
yarn verify-graphql-code
|
||||
- name: Build tarballs
|
||||
working-directory: ${{ github.workspace }}/packages/eas-cli
|
||||
run: |
|
||||
@@ -64,11 +71,11 @@ jobs:
|
||||
# Use macOS to get the correct platform specific dependencies
|
||||
runs-on: macos-latest
|
||||
needs: release
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 20
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ./.github/actions/setup-mise
|
||||
- run: brew install python-setuptools
|
||||
env:
|
||||
HOMEBREW_NO_AUTO_UPDATE: 1
|
||||
@@ -107,11 +114,11 @@ jobs:
|
||||
name: Build for Windows
|
||||
runs-on: ubuntu-latest
|
||||
needs: release
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 20
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ./.github/actions/setup-mise
|
||||
- run: yarn install --frozen-lockfile --check-files
|
||||
- run: sudo apt-get install nsis
|
||||
- run: yarn build
|
||||
@@ -147,46 +154,42 @@ jobs:
|
||||
name: Publish to npm
|
||||
runs-on: ubuntu-latest
|
||||
needs: [build-linux, build-mac, build-windows]
|
||||
permissions:
|
||||
id-token: write
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
registry-url: 'https://registry.npmjs.org/'
|
||||
scope: 'expo'
|
||||
node-version: 20
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ./.github/actions/setup-mise
|
||||
- name: Install dependencies
|
||||
run: yarn install --frozen-lockfile --check-files
|
||||
- name: Build
|
||||
run: yarn build
|
||||
- name: Publish packages to npm
|
||||
run: yarn lerna publish from-package --yes --no-verify-access
|
||||
- name: Add latest-eas-build-staging tag
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_ACCESS_TOKEN }}
|
||||
- name: Add latest-eas-build-staging tag
|
||||
run: |
|
||||
VERSION=$(cat lerna.json | jq -r .version)
|
||||
echo //registry.npmjs.org/:_authToken=$NODE_AUTH_TOKEN > ~/.npmrc
|
||||
npm dist-tag add eas-cli@$VERSION latest-eas-build-staging
|
||||
- name: Add latest-eas-build tag
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_ACCESS_TOKEN }}
|
||||
- name: Add latest-eas-build tag
|
||||
run: |
|
||||
VERSION=$(cat lerna.json | jq -r .version)
|
||||
echo //registry.npmjs.org/:_authToken=$NODE_AUTH_TOKEN > ~/.npmrc
|
||||
npm dist-tag add eas-cli@$VERSION latest-eas-build
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_ACCESS_TOKEN }}
|
||||
release-changelog:
|
||||
name: Update changelog and publish release
|
||||
runs-on: ubuntu-latest
|
||||
needs: publish-to-npm
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
token: ${{ secrets.EXPO_BOT_PAT }}
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 20
|
||||
- uses: ./.github/actions/setup-mise
|
||||
- name: Install dependencies
|
||||
working-directory: ./scripts
|
||||
run: yarn install --frozen-lockfile --check-files
|
||||
@@ -206,7 +209,7 @@ jobs:
|
||||
git config --global user.email "[email protected]"
|
||||
git config --global user.name "Expo CI"
|
||||
git add CHANGELOG.md
|
||||
git commit -m "update CHANGELOG.md"
|
||||
git commit --allow-empty -m "update CHANGELOG.md"
|
||||
git push
|
||||
- name: Prepare changelog for Slack
|
||||
run: |
|
||||
|
||||
@@ -10,9 +10,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 20
|
||||
- uses: ./.github/actions/setup-mise
|
||||
- uses: preactjs/compressed-size-action@v2
|
||||
with:
|
||||
build-script: 'build:tarballs:linux'
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
name: Ensure GraphQL schema and generated code is up-to-date
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
schedule:
|
||||
- cron: '0 8 * * *' # 8:00 AM UTC time every day
|
||||
|
||||
concurrency:
|
||||
group: test-graphql-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Ensure GraphQL schema and generated code is up-to-date
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Setup tools
|
||||
uses: ./.github/actions/setup-mise
|
||||
- run: yarn install --frozen-lockfile
|
||||
- run: yarn verify-graphql-code
|
||||
working-directory: packages/eas-cli
|
||||
@@ -22,9 +22,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 20
|
||||
- name: Setup tools
|
||||
uses: ./.github/actions/setup-mise
|
||||
- run: yarn install --frozen-lockfile --check-files
|
||||
- run: yarn typecheck
|
||||
@@ -15,7 +15,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
node: ['18', '20', '22']
|
||||
node: ['20', '22', '24']
|
||||
include:
|
||||
- node: '20'
|
||||
coverage: true
|
||||
@@ -31,12 +31,26 @@ jobs:
|
||||
- run: |
|
||||
git config --global user.email "[email protected]"
|
||||
git config --global user.name "Test"
|
||||
- run: yarn test
|
||||
if: ${{ !matrix.coverage }}
|
||||
- name: Fetch base commit for PR diffing
|
||||
if: ${{ github.event_name == 'pull_request' }}
|
||||
# Lerna needs the base commit to compute affected packages.
|
||||
run: git fetch --no-tags --depth=1 origin ${{ github.event.pull_request.base.sha }}
|
||||
- name: Run tests (affected)
|
||||
# PRs: only run tests for changed packages + dependents.
|
||||
if: ${{ !matrix.coverage && github.event_name == 'pull_request' }}
|
||||
run: yarn lerna run test --since ${{ github.event.pull_request.base.sha }} --include-dependents
|
||||
env:
|
||||
NODE_OPTIONS: --max-old-space-size=8192
|
||||
- run: yarn test --coverage
|
||||
- name: Run tests (all)
|
||||
# Pushes to main: run all package tests.
|
||||
if: ${{ !matrix.coverage && github.event_name == 'push' }}
|
||||
run: yarn lerna run test
|
||||
env:
|
||||
NODE_OPTIONS: --max-old-space-size=8192
|
||||
- name: Run tests (coverage)
|
||||
# Coverage on PRs and main; per-package reports are uploaded to Codecov.
|
||||
if: ${{ matrix.coverage }}
|
||||
run: yarn lerna run test -- -- --coverage
|
||||
env:
|
||||
# Oclif seems to cause OOM errors when testing with coverage enabled.
|
||||
# Remove this whenever we solved those OOM errors.
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
type: string
|
||||
required: false
|
||||
description: 'Version number for the release'
|
||||
dry_run:
|
||||
type: boolean
|
||||
required: true
|
||||
default: true
|
||||
description: 'Run the release in dry run mode, without pushing any changes'
|
||||
|
||||
name: Manually trigger a release
|
||||
|
||||
jobs:
|
||||
release:
|
||||
name: Trigger a release
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.EXPO_BOT_PAT }}
|
||||
INPUT_VERSION: ${{ github.event.inputs.version }}
|
||||
INPUT_DRY_RUN: ${{ github.event.inputs.dry_run }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: main
|
||||
fetch-depth: 0
|
||||
token: ${{ secrets.EXPO_BOT_PAT }}
|
||||
- uses: ./.github/actions/setup-mise
|
||||
- run: |
|
||||
# Lerna will not detect changes correctly without having all tags
|
||||
git fetch --depth=1 origin +refs/tags/*:refs/tags/*
|
||||
# Checkout the main branch
|
||||
git checkout main
|
||||
git config --global user.email "[email protected]"
|
||||
git config --global user.name "Expo CI"
|
||||
yarn install --frozen-lockfile --check-files
|
||||
yarn build
|
||||
if [[ "$INPUT_VERSION" == "" ]]; then
|
||||
echo "Releasing with default version"
|
||||
yarn release
|
||||
else
|
||||
echo "Releasing with version $INPUT_VERSION"
|
||||
yarn release $INPUT_VERSION
|
||||
fi
|
||||
@@ -1,81 +0,0 @@
|
||||
name: Open a PR bumping eas-build modules
|
||||
|
||||
on: workflow_dispatch
|
||||
|
||||
jobs:
|
||||
open-pr:
|
||||
name: Open a PR bumping eas-build modules
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: main
|
||||
token: ${{ secrets.EXPO_BOT_PAT }}
|
||||
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
- run: yarn install --frozen-lockfile
|
||||
|
||||
- name: Configure git
|
||||
run: |
|
||||
git config --global user.email "[email protected]"
|
||||
git config --global user.name "Expo Bot"
|
||||
|
||||
- name: Upgrade eas-build modules
|
||||
run: |
|
||||
for DIR in packages/eas-cli packages/eas-json; do
|
||||
pushd $DIR >/dev/null 2>&1
|
||||
# packages are ordered according to output of `lerna list --toposort`
|
||||
# https://github.com/expo/turtle-v2/pull/568
|
||||
for PACKAGE in "@expo/downloader" "@expo/eas-build-job" "@expo/logger" "@expo/template-file" "@expo/steps" "@expo/turtle-spawn" "@expo/build-tools"; do
|
||||
if grep -q $PACKAGE package.json; then
|
||||
echo "[$DIR] yarn --exact add $PACKAGE"
|
||||
yarn add --exact $PACKAGE
|
||||
fi
|
||||
done
|
||||
popd >/dev/null 2>&1
|
||||
done
|
||||
|
||||
- name: Set a branch name
|
||||
id: bump-branch-name
|
||||
run: |
|
||||
HASH="$(echo $RANDOM | md5sum | head -c 10)"
|
||||
BRANCH_NAME="@expo-bot/eas-build-bump-$HASH"
|
||||
|
||||
echo "BRANCH_NAME=$BRANCH_NAME" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Create a branch, commit, and push
|
||||
run: |
|
||||
BRANCH_NAME="${{ steps.bump-branch-name.outputs.BRANCH_NAME }}"
|
||||
|
||||
git checkout -b $BRANCH_NAME
|
||||
git add .
|
||||
git commit -m "Upgrade eas-build dependencies"
|
||||
git push origin $BRANCH_NAME
|
||||
|
||||
- name: Open a PR to main
|
||||
uses: repo-sync/pull-request@v2
|
||||
id: open-pr
|
||||
with:
|
||||
source_branch: '${{ steps.bump-branch-name.outputs.BRANCH_NAME }}'
|
||||
destination_branch: 'main'
|
||||
pr_title: 'Upgrade eas-build dependencies'
|
||||
pr_body: ':arrow_up: Upgrades [`eas-build`](https://github.com/expo/eas-build) dependencies.'
|
||||
pr_reviewer: 'radoslawkrzemien,sjchmiela,khamilowicz'
|
||||
github_token: ${{ secrets.EXPO_BOT_PAT }}
|
||||
|
||||
- uses: actions/github-script@v3
|
||||
with:
|
||||
github-token: ${{ secrets.EXPO_BOT_PAT }}
|
||||
script: |
|
||||
github.issues.createComment({
|
||||
issue_number: ${{ steps.open-pr.outputs.pr_number }},
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
body: "/changelog-entry chore Upgrade [`eas-build`](https://github.com/expo/eas-build) dependencies."
|
||||
})
|
||||
@@ -0,0 +1,160 @@
|
||||
name: system-tests
|
||||
|
||||
on:
|
||||
workflow_dispatch: {}
|
||||
workflow_call: {}
|
||||
|
||||
jobs:
|
||||
system-tests:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
EXPO_TOKEN: ${{ secrets.STAGING_EXPO_DEV_EXPO_SERVICES_GITHUB_ROBOT_ACCESS_TOKEN }}
|
||||
EXPO_STAGING: "1"
|
||||
steps:
|
||||
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4
|
||||
|
||||
- uses: ./.github/actions/setup-mise
|
||||
|
||||
- name: Create test app
|
||||
run: npx -y create-expo-app@latest ${{ runner.temp }}/test-app --template tabs
|
||||
|
||||
- name: Link to EAS project
|
||||
working-directory: ${{ runner.temp }}/test-app
|
||||
run: npx -y eas-cli@latest init --id 6ae8028d-4e6d-4912-8eb2-87d4e6122880 --force --non-interactive
|
||||
|
||||
- name: Add bundle identifiers to app.json
|
||||
working-directory: ${{ runner.temp }}/test-app
|
||||
run: |
|
||||
jq '.expo.ios.bundleIdentifier = "dev.expo.workersystemtests" | .expo.android.package = "dev.expo.workersystemtests"' app.json > app.json.tmp
|
||||
mv app.json.tmp app.json
|
||||
|
||||
- name: Configure EAS Build
|
||||
working-directory: ${{ runner.temp }}/test-app
|
||||
run: npx -y eas-cli@latest build:configure --platform all
|
||||
|
||||
- name: Add system-test profile to eas.json
|
||||
working-directory: ${{ runner.temp }}/test-app
|
||||
run: |
|
||||
jq '.build["system-test"] = { "ios": { "simulator": true }, "android": { "withoutCredentials": true } }' eas.json > eas.json.tmp
|
||||
mv eas.json.tmp eas.json
|
||||
|
||||
- name: Setup EAS workflow
|
||||
working-directory: ${{ runner.temp }}/test-app
|
||||
run: |
|
||||
mkdir -p .eas/workflows
|
||||
cp ${{ github.workspace }}/.github/fixtures/eas-workflow-build-android.yml .eas/workflows/build-android.yml
|
||||
|
||||
- name: Run iOS build and Android workflow concurrently
|
||||
working-directory: ${{ runner.temp }}/test-app
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
BUILD_JSON=$(npx -y eas-cli@latest build --platform ios --profile system-test --non-interactive --no-wait --json)
|
||||
BUILD_ID=$(echo "$BUILD_JSON" | jq -r 'if type=="array" then .[0].id // .[0].build.id // .[0].buildId else .id // .build.id // .buildId end // empty')
|
||||
if [ -z "$BUILD_ID" ]; then
|
||||
echo "Error: could not determine build ID"
|
||||
echo "$BUILD_JSON" | jq .
|
||||
exit 1
|
||||
fi
|
||||
echo "BUILD_ID=$BUILD_ID" >> "$GITHUB_ENV"
|
||||
echo "Resolved BUILD_ID=$BUILD_ID"
|
||||
|
||||
WORKFLOW_JSON=$(npx -y eas-cli@latest workflow:run build-android.yml --non-interactive --no-wait --json)
|
||||
WORKFLOW_RUN_ID=$(echo "$WORKFLOW_JSON" | jq -r '.id // .workflowRun.id // .workflowRunId // empty')
|
||||
if [ -z "$WORKFLOW_RUN_ID" ]; then
|
||||
echo "Error: could not determine workflow run ID"
|
||||
echo "$WORKFLOW_JSON" | jq .
|
||||
exit 1
|
||||
fi
|
||||
echo "WORKFLOW_RUN_ID=$WORKFLOW_RUN_ID" >> "$GITHUB_ENV"
|
||||
echo "Resolved WORKFLOW_RUN_ID=$WORKFLOW_RUN_ID"
|
||||
|
||||
is_terminal_build_status() {
|
||||
case "$1" in
|
||||
FINISHED|ERRORED|CANCELED) return 0 ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
is_terminal_workflow_status() {
|
||||
case "$1" in
|
||||
SUCCESS|FAILURE|CANCELED|ACTION_REQUIRED) return 0 ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
BUILD_STATUS=""
|
||||
WORKFLOW_STATUS=""
|
||||
TIMEOUT_SECONDS=1800
|
||||
START_TIME=$(date +%s)
|
||||
|
||||
while true; do
|
||||
ELAPSED=$(( $(date +%s) - START_TIME ))
|
||||
if [ "$ELAPSED" -ge "$TIMEOUT_SECONDS" ]; then
|
||||
echo "Timeout: polling exceeded 30 minutes"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
BUILD_STATUS=$(npx -y eas-cli@latest build:view "$BUILD_ID" --json | jq -r '.status // empty')
|
||||
WORKFLOW_STATUS=$(npx -y eas-cli@latest workflow:view "$WORKFLOW_RUN_ID" --non-interactive --json | jq -r '.status // empty')
|
||||
|
||||
echo "iOS build status: $BUILD_STATUS"
|
||||
echo "Android workflow status: $WORKFLOW_STATUS"
|
||||
|
||||
if is_terminal_build_status "$BUILD_STATUS" && is_terminal_workflow_status "$WORKFLOW_STATUS"; then
|
||||
break
|
||||
fi
|
||||
|
||||
sleep 7
|
||||
done
|
||||
|
||||
EXIT_CODE=0
|
||||
|
||||
if [ "$BUILD_STATUS" != "FINISHED" ]; then
|
||||
echo "Build did not finish successfully: $BUILD_STATUS"
|
||||
EXIT_CODE=1
|
||||
fi
|
||||
|
||||
if [ "$WORKFLOW_STATUS" != "SUCCESS" ]; then
|
||||
echo "Workflow did not finish successfully: $WORKFLOW_STATUS"
|
||||
EXIT_CODE=1
|
||||
fi
|
||||
|
||||
exit "$EXIT_CODE"
|
||||
|
||||
- name: Cancel EAS runs on workflow cancellation
|
||||
if: always() && (cancelled() || failure())
|
||||
working-directory: ${{ runner.temp }}/test-app
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
if [ -n "${BUILD_ID:-}" ]; then
|
||||
echo "Canceling EAS build: $BUILD_ID"
|
||||
BUILD_CANCEL_OUTPUT=$(npx -y eas-cli@latest build:cancel "$BUILD_ID" --non-interactive 2>&1 || true)
|
||||
if [ -n "$BUILD_CANCEL_OUTPUT" ]; then
|
||||
echo "$BUILD_CANCEL_OUTPUT"
|
||||
else
|
||||
echo "build:cancel produced no output"
|
||||
fi
|
||||
BUILD_STATUS=$(npx -y eas-cli@latest build:view "$BUILD_ID" --json 2>/dev/null | jq -r '.status // empty' || true)
|
||||
if [ -n "$BUILD_STATUS" ]; then
|
||||
echo "Post-cancel build status: $BUILD_STATUS"
|
||||
else
|
||||
echo "Post-cancel build status unavailable"
|
||||
fi
|
||||
fi
|
||||
if [ -n "${WORKFLOW_RUN_ID:-}" ]; then
|
||||
echo "Canceling EAS workflow run: $WORKFLOW_RUN_ID"
|
||||
WORKFLOW_CANCEL_OUTPUT=$(npx -y eas-cli@latest workflow:cancel "$WORKFLOW_RUN_ID" --non-interactive 2>&1 || true)
|
||||
if [ -n "$WORKFLOW_CANCEL_OUTPUT" ]; then
|
||||
echo "$WORKFLOW_CANCEL_OUTPUT"
|
||||
else
|
||||
echo "workflow:cancel produced no output"
|
||||
fi
|
||||
WORKFLOW_STATUS=$(npx -y eas-cli@latest workflow:view "$WORKFLOW_RUN_ID" --non-interactive --json 2>/dev/null | jq -r '.status // empty' || true)
|
||||
if [ -n "$WORKFLOW_STATUS" ]; then
|
||||
echo "Post-cancel workflow status: $WORKFLOW_STATUS"
|
||||
else
|
||||
echo "Post-cancel workflow status unavailable"
|
||||
fi
|
||||
fi
|
||||
@@ -0,0 +1,80 @@
|
||||
name: worker
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- "packages/worker/**"
|
||||
- "packages/build-tools/**"
|
||||
- "packages/eas-build-job/**"
|
||||
- "packages/steps/**"
|
||||
- "packages/logger/**"
|
||||
- "packages/downloader/**"
|
||||
- "packages/template-file/**"
|
||||
- "packages/turtle-spawn/**"
|
||||
- .github/workflows/worker.yml
|
||||
- .github/workflows/build-and-deploy-worker.yml
|
||||
- yarn.lock
|
||||
|
||||
concurrency:
|
||||
group: worker-${{ github.ref }}-${{ github.event_name }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
worker-checks:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4
|
||||
|
||||
- uses: ./.github/actions/setup-mise
|
||||
|
||||
- run: yarn install --frozen-lockfile --check-files
|
||||
|
||||
- run: yarn typecheck
|
||||
|
||||
- name: Run unit tests
|
||||
working-directory: packages/worker
|
||||
run: yarn test:unit
|
||||
|
||||
- name: Run integration tests
|
||||
working-directory: packages/worker
|
||||
run: yarn test:integration
|
||||
|
||||
worker-build:
|
||||
needs:
|
||||
- worker-checks
|
||||
uses: ./.github/workflows/build-and-deploy-worker.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
environment: ${{ github.ref == 'refs/heads/main' && 'staging' || '' }}
|
||||
|
||||
system-tests-staging:
|
||||
if: github.ref == 'refs/heads/main' && !failure() && !cancelled()
|
||||
needs:
|
||||
- worker-build
|
||||
- worker-checks
|
||||
uses: ./.github/workflows/worker-system-tests.yml
|
||||
secrets: inherit
|
||||
|
||||
deploy-worker-production:
|
||||
if: github.ref == 'refs/heads/main' && !failure() && !cancelled()
|
||||
needs:
|
||||
- system-tests-staging
|
||||
uses: ./.github/workflows/build-and-deploy-worker.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
environment: production
|
||||
|
||||
notify-slack:
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- deploy-worker-production
|
||||
if: github.ref == 'refs/heads/main' && always() && !cancelled()
|
||||
steps:
|
||||
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4
|
||||
|
||||
- name: Notify Slack
|
||||
uses: ./.github/internal-actions/notify-slack-on-fail-or-recover
|
||||
with:
|
||||
channel: turtle-bots
|
||||
slack_webhook: ${{ secrets.SLACK_EAS_WORKFLOWS_WEBHOOK }}
|
||||
@@ -3,11 +3,17 @@ packages/*/build
|
||||
packages/eas-cli/dist
|
||||
packages/eas-cli/tmp
|
||||
scripts/build
|
||||
dist
|
||||
|
||||
# TypeScript
|
||||
**/*.tsbuildinfo
|
||||
|
||||
# Code editors
|
||||
.idea
|
||||
.history
|
||||
.vscode
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# macOS
|
||||
.DS_Store
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
# AGENTS.md
|
||||
|
||||
When working with this repository follow instructions from CLAUDE.md.
|
||||
|
||||
- [./CLAUDE.md](./CLAUDE.md)
|
||||
|
||||
+385
-4
@@ -6,15 +6,396 @@ This is the log of notable changes to EAS CLI and related packages.
|
||||
|
||||
### 🛠 Breaking changes
|
||||
|
||||
- Drop support for Node 18 and Node 19. Require Node 20+. ([#3343](https://github.com/expo/eas-cli/pull/3343) by [@sjchmiela](https://github.com/sjchmiela))
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
|
||||
- Hide progress bar in build credits warning when usage reaches 100%. ([#3371](https://github.com/expo/eas-cli/pull/3371) by [@mackenco](https://github.com/mackenco))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Fix `--non-interactive` mode prompting for git initialization. Now auto-initializes git with an initial commit when needed. Set `EAS_NO_VCS=1` to skip. ([#3387](https://github.com/expo/eas-cli/pull/3387) by [@evanbacon](https://github.com/evanbacon))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Upgrade `tar` to v7. ([#3327](https://github.com/expo/eas-cli/pull/3327) by [@KarolRzeminski](https://github.com/KarolRzeminski))
|
||||
|
||||
## [16.32.0](https://github.com/expo/eas-cli/releases/tag/v16.32.0) - 2026-01-30
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Add `--browser` flag to `eas login` for browser-based authentication. ([#3312](https://github.com/expo/eas-cli/pull/3312) by [@byronkarlen](https://github.com/byronkarlen))
|
||||
- List available options in non-interactive mode errors instead of generic "unable to select" messages, making CLI usable by agents and scripts. ([#3359](https://github.com/expo/eas-cli/pull/3359) by [@EvanBacon](https://github.com/EvanBacon))
|
||||
- Hidden beta version of new `eas account:usage` command for viewing account usage and billing estimates. ([#3334](https://github.com/expo/eas-cli/pull/3334) by [@douglowder](https://github.com/douglowder), [@EvanBacon](https://github.com/EvanBacon))
|
||||
- Add `--no-bytecode` and `--source-maps [mode]` flags to `eas update`. ([#3339](https://github.com/expo/eas-cli/pull/3339) by [@brentvatne](https://github.com/brentvatne))
|
||||
- Add App Clip bundle identifier registration support for multi-target iOS builds. ([#3300](https://github.com/expo/eas-cli/pull/3300) by [@evanbacon](https://github.com/evanbacon))
|
||||
- Warn in `eas build` when creating a production build from an app that uses Expo Go for development ([#3073](https://github.com/expo/eas-cli/pull/3073) by [@vonovak](https://github.com/vonovak))
|
||||
- Add `--runtime-version` and `--platform` filters to `eas update:list`. ([#3261](https://github.com/expo/eas-cli/pull/3261) by [@HarelSultan](https://github.com/HarelSultan))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Fix `--source-maps` flag compatibility with older SDKs that only support it as a boolean flag. ([#3341](https://github.com/expo/eas-cli/pull/3341) by [@brentvatne](https://github.com/brentvatne))
|
||||
- Use `--dump-sourcemaps` as fallback when `--source-maps` is not provided to `eas update`, for backwards compatibility. ([8cc324e1](https://github.com/expo/eas-cli/commit/8cc324e1) by [@brentvatne](https://github.com/brentvatne))
|
||||
- Fix `metadata:pull` failing for apps with only a live version by falling back to live app version and info. ([#3299](https://github.com/expo/eas-cli/pull/3299) by [@EvanBacon](https://github.com/EvanBacon))
|
||||
- eas init should fix and validate project name and slug. ([#3277](https://github.com/expo/eas-cli/pull/3277) by [@douglowder](https://github.com/douglowder))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Test with node 24. ([#3270](https://github.com/expo/eas-cli/pull/3270) by [@wschurman](https://github.com/wschurman))
|
||||
- Delete channel in background. ([#3278](https://github.com/expo/eas-cli/pull/3278) by [@quinlanj](https://github.com/quinlanj))
|
||||
- Add warning to channel:delete. ([#3335](https://github.com/expo/eas-cli/pull/3335) by [@quinlanj](https://github.com/quinlanj))
|
||||
|
||||
## [16.28.0](https://github.com/expo/eas-cli/releases/tag/v16.28.0) - 2025-11-20
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- `eas:new` sorts organization accounts first in account selection. ([#3256](https://github.com/expo/eas-cli/pull/3256) by [@mackenco](https://github.com/mackenco))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Fix `eas:build` bug where overages are checked before config is ready. ([#3260](https://github.com/expo/eas-cli/pull/3260) by [@mackenco](https://github.com/mackenco))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Delete branch in background. ([#3258](https://github.com/expo/eas-cli/pull/3258) by [@quinlanj](https://github.com/quinlanj))
|
||||
|
||||
## [16.27.0](https://github.com/expo/eas-cli/releases/tag/v16.27.0) - 2025-11-13
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Kick off update patch generation if enabled - this is feature is in preview, don't use it for prod yet please. ([#3250](https://github.com/expo/eas-cli/pull/3250) by [@quinlanj](https://github.com/quinlanj))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Fix workflow:status when run ID passed in. ([#3253](https://github.com/expo/eas-cli/pull/3253) by [@douglowder](https://github.com/douglowder))
|
||||
- Fix updating file env vars with `env:update --non-interactive`. ([#3249](https://github.com/expo/eas-cli/pull/3249) by [@kadikraman](https://github.com/kadikraman))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Fix yarn watch. ([#3251](https://github.com/expo/eas-cli/pull/3251) by [@quinlanj](https://github.com/quinlanj))
|
||||
- Remove the **node_modules** installation requirement for `workflow:run` command. ([#3254](https://github.com/expo/eas-cli/pull/3254) by [@kudo](https://github.com/kudo))
|
||||
|
||||
## [16.26.0](https://github.com/expo/eas-cli/releases/tag/v16.26.0) - 2025-10-29
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- New command `workflow:status`. ([#3242](https://github.com/expo/eas-cli/pull/3242) by [@douglowder](https://github.com/douglowder))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- `eas:new` cleanup (remove `hidden` tag, fix README error, output cleanup). ([#3243](https://github.com/expo/eas-cli/pull/3243) by [@mackenco](https://github.com/mackenco))
|
||||
|
||||
## [16.25.1](https://github.com/expo/eas-cli/releases/tag/v16.25.1) - 2025-10-28
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Remove preview label from EAS Hosting commands ([#3241](https://github.com/expo/eas-cli/pull/3241) by [@kitten](https://github.com/kitten))
|
||||
|
||||
## [16.25.0](https://github.com/expo/eas-cli/releases/tag/v16.25.0) - 2025-10-27
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- `eas:new` command changes (project name prompt, output cleanup, additional project files, additional prompts). ([#3235](https://github.com/expo/eas-cli/pull/3235) by [@mackenco](https://github.com/mackenco))
|
||||
- `eas:build` command displays a warning when account is near it's build limit. ([#3236](https://github.com/expo/eas-cli/pull/3236) by [@mackenco](https://github.com/mackenco))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- workflow:run can use --wait and --json together. ([#3239](https://github.com/expo/eas-cli/pull/3239) by [@douglowder](https://github.com/douglowder))
|
||||
- Add missing env for local builds with latest plugin. ([#3237](https://github.com/expo/eas-cli/pull/3237) by [@douglowder](https://github.com/douglowder))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Document the default values for `track` and `releaseStatus`. ([#3229](https://github.com/expo/eas-cli/pull/3229) by [@kadikraman](https://github.com/kadikraman))
|
||||
|
||||
## [16.24.1](https://github.com/expo/eas-cli/releases/tag/v16.24.1) - 2025-10-19
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Revert change in ora version. ([#3231](https://github.com/expo/eas-cli/pull/3231) by [@douglowder](https://github.com/douglowder))
|
||||
|
||||
## [16.24.0](https://github.com/expo/eas-cli/releases/tag/v16.24.0) - 2025-10-19
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- workflow:run --wait shows periodic job and step status updates. ([#3227](https://github.com/expo/eas-cli/pull/3227) by [@douglowder](https://github.com/douglowder))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- workflow:logs should show logs from build and repack jobs. ([#3226](https://github.com/expo/eas-cli/pull/3226) by [@douglowder](https://github.com/douglowder))
|
||||
|
||||
## [16.23.1](https://github.com/expo/eas-cli/releases/tag/v16.23.1) - 2025-10-16
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Multiple bugfixes and small features for the `eas:new` command: handle project name and directory generation more smoothly; support `bun` as a package manager; add `--package-manager` flag with a default to `npm`; ensure workflow files are in the correct location after build; update the generated project's README with the provided package manager; ensure `app.json`'s `scheme` is valid; silence lots of noisy output. ([#3205](https://github.com/expo/eas-cli/pull/3205) by [@mackenco](https://github.com/mackenco))
|
||||
|
||||
## [16.23.0](https://github.com/expo/eas-cli/releases/tag/v16.23.0) - 2025-10-14
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Support custom environment variable environments ([#3220](https://github.com/expo/eas-cli/pull/3220) by [@kadikraman](https://github.com/kadikraman))
|
||||
- Add --force flag to eas env:push command to bypass confirmation prompts. ([#3135](https://github.com/expo/eas-cli/pull/3135) by [@giorgiofellipe](https://github.com/giorgiofellipe))
|
||||
|
||||
## [16.22.0](https://github.com/expo/eas-cli/releases/tag/v16.22.0) - 2025-10-13
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Improvements to workflow:create. ([#3194](https://github.com/expo/eas-cli/pull/3194) by [@douglowder](https://github.com/douglowder))
|
||||
|
||||
## [16.21.0](https://github.com/expo/eas-cli/releases/tag/v16.21.0) - 2025-10-13
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Support `_expo/.routes.json` metadata file exported by `@expo/cli` for headers configuration on static deployments ([#3211](https://github.com/expo/eas-cli/pull/3211) by [@kitten](https://github.com/kitten))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Add thorough tests for the env commands ([#3217](https://github.com/expo/eas-cli/pull/3217) by [@kadikraman](https://github.com/kadikraman))
|
||||
|
||||
## [16.20.4](https://github.com/expo/eas-cli/releases/tag/v16.20.4) - 2025-10-08
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Allow workflow:run to run a workflow from a GitHub ref. ([#3203](https://github.com/expo/eas-cli/pull/3203) by [@douglowder](https://github.com/douglowder))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Fix --environment flag in eas exec and pull ([#3216](https://github.com/expo/eas-cli/pull/3216) by [@kadikraman](https://github.com/kadikraman))
|
||||
|
||||
## [16.20.3](https://github.com/expo/eas-cli/releases/tag/v16.20.3) - 2025-10-07
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Fix --environment flag in eas env commands ([#3213](https://github.com/expo/eas-cli/pull/3213) by [@kadikraman](https://github.com/kadikraman))
|
||||
|
||||
## [16.20.2](https://github.com/expo/eas-cli/releases/tag/v16.20.2) - 2025-10-07
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Create an `EnvironmentVariableEnvironment` enum and use it instead of the graphQL Enum ([#3208](https://github.com/expo/eas-cli/pull/3208) by [@kadikraman](https://github.com/kadikraman))
|
||||
|
||||
## [16.20.1](https://github.com/expo/eas-cli/releases/tag/v16.20.1) - 2025-10-03
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- skip syncing capabilities with settings if they don't contain the `enabled` attribute ([#3201](https://github.com/expo/eas-cli/pull/3201) by [@vonovak](https://github.com/vonovak))
|
||||
|
||||
## [16.20.0](https://github.com/expo/eas-cli/releases/tag/v16.20.0) - 2025-09-30
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Improvements to workflow:validate. ([#3165](https://github.com/expo/eas-cli/pull/3165) by [@douglowder](https://github.com/douglowder))
|
||||
- Add `eas:new` command behind `hidden` flag. ([#3196](https://github.com/expo/eas-cli/pull/3196) by [@mackenco](https://github.com/mackenco))
|
||||
|
||||
## [16.19.3](https://github.com/expo/eas-cli/releases/tag/v16.19.3) - 2025-09-17
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Temporarily remove warnings when no compatible build found, need to refine the message. ([#3182](https://github.com/expo/eas-cli/pull/3182) by [@brentvatne](https://github.com/brentvatne))
|
||||
|
||||
## [16.19.2](https://github.com/expo/eas-cli/releases/tag/v16.19.2) - 2025-09-15
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Add artifact URLs to workflow:view. ([#3175](https://github.com/expo/eas-cli/pull/3175) by [@douglowder](https://github.com/douglowder))
|
||||
|
||||
## [16.19.1](https://github.com/expo/eas-cli/releases/tag/v16.19.1) - 2025-09-12
|
||||
|
||||
## [16.19.0](https://github.com/expo/eas-cli/releases/tag/v16.19.0) - 2025-09-12
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Allow any string as Android release `track`, not just the `production`, `internal`, `alpha`, `beta`. ([#3163](https://github.com/expo/eas-cli/pull/3163) by [@sjchmiela](https://github.com/sjchmiela))
|
||||
|
||||
## [16.18.1](https://github.com/expo/eas-cli/releases/tag/v16.18.1) - 2025-09-10
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- CLI debug log should respect DEBUG=`eas:*`. ([#3162](https://github.com/expo/eas-cli/pull/3162) by [@douglowder](https://github.com/douglowder))
|
||||
- Better description for update:rollback. ([#3154](https://github.com/expo/eas-cli/pull/3154) by [@douglowder](https://github.com/douglowder))
|
||||
|
||||
## [16.18.0](https://github.com/expo/eas-cli/releases/tag/v16.18.0) - 2025-08-27
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Show fingerprints in build:view and build:list commands. ([#3137](https://github.com/expo/eas-cli/pull/3137) by [@douglowder](https://github.com/douglowder))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Make `EXPO_PUBLIC_` env vars plain text, rest sensitive ([#3121](https://github.com/expo/eas-cli/pull/3121) by [@kadikraman](https://github.com/kadikraman))
|
||||
|
||||
## [16.17.4](https://github.com/expo/eas-cli/releases/tag/v16.17.4) - 2025-07-31
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Introduce new release process and workflow. ([#3119](https://github.com/expo/eas-cli/pull/3119) by [@douglowder](https://github.com/douglowder))
|
||||
- SSO Login: print browser url ([#3113](https://github.com/expo/eas-cli/pull/3113) by [@BackSlasher](https://github.com/BackSlasher))
|
||||
|
||||
## [16.17.2](https://github.com/expo/eas-cli/releases/tag/v16.17.2) - 2025-07-23
|
||||
|
||||
## [16.17.1](https://github.com/expo/eas-cli/releases/tag/v16.17.1) - 2025-07-23
|
||||
|
||||
## [16.17.0](https://github.com/expo/eas-cli/releases/tag/v16.17.0) - 2025-07-18
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Add `deploy:delete` for deleting worker deployments ([#3106](https://github.com/expo/eas-cli/pull/3106) by [@kadikraman](https://github.com/kadikraman))
|
||||
- Add `deploy:alias:delete` for deleting worker deployment aliases ([#3107](https://github.com/expo/eas-cli/pull/3107) by [@kadikraman](https://github.com/kadikraman))
|
||||
|
||||
## [16.16.0](https://github.com/expo/eas-cli/releases/tag/v16.16.0) - 2025-07-16
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Add workflow:view and workflow:logs commands. ([#3090](https://github.com/expo/eas-cli/pull/3090) by [@douglowder](https://github.com/douglowder))
|
||||
- Add support for providing inputs to new workflow runs ([#3095](https://github.com/expo/eas-cli/pull/3095) by [@sjchmiela](https://github.com/sjchmiela))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Refactor capabilities syncing to avoid provisioning profiles becoming invalid. ([#3088](https://github.com/expo/eas-cli/pull/3088) by [@vonovak](https://github.com/vonovak))
|
||||
- Avoid corrupted EAS Hosting batch upload bodies and double-check length and checksums while uploading. ([#3103](https://github.com/expo/eas-cli/pull/3103) by [@kitten](https://github.com/kitten))
|
||||
|
||||
## [16.15.0](https://github.com/expo/eas-cli/releases/tag/v16.15.0) - 2025-07-10
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Implement new EAS Hosting asset upload protocol, which improves performance by grouping files into batched upload API calls ([#3085](https://github.com/expo/eas-cli/pull/3085) by [@kitten](https://github.com/kitten))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Update the EAS Hosting worker deployment progress indicator to use a progress bar for asset uploads instead ([#3099](https://github.com/expo/eas-cli/pull/3099) by [@kitten](https://github.com/kitten))
|
||||
|
||||
## [16.14.1](https://github.com/expo/eas-cli/releases/tag/v16.14.1) - 2025-07-08
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Update apple team info as needed. ([#3096](https://github.com/expo/eas-cli/pull/3096) by [@quinlanj](https://github.com/quinlanj))
|
||||
|
||||
## [16.14.0](https://github.com/expo/eas-cli/releases/tag/v16.14.0) - 2025-07-07
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Add update:revert-update-rollout command. ([#3068](https://github.com/expo/eas-cli/pull/3068) by [@wschurman](https://github.com/wschurman))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Remove unreleased `link` and `unlink` commands from `eas env`. ([#3092](https://github.com/expo/eas-cli/pull/3092) by [@kadikraman](https://github.com/kadikraman))
|
||||
- Add new CI to verify GraphQL generation. ([#3093](https://github.com/expo/eas-cli/pull/3093) by [@douglowder](https://github.com/douglowder))
|
||||
- Update GraphQL generation to look in all src directories. ([#3097](https://github.com/expo/eas-cli/pull/3097) by [@wschurman](https://github.com/wschurman))
|
||||
|
||||
## [16.13.4](https://github.com/expo/eas-cli/releases/tag/v16.13.4) - 2025-07-04
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Remove hidden flag from `eas upload`. ([#3014](https://github.com/expo/eas-cli/pull/3014) by [@gabrieldonadel](https://github.com/gabrieldonadel))
|
||||
|
||||
## [16.13.3](https://github.com/expo/eas-cli/releases/tag/v16.13.3) - 2025-07-01
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Fix suggested command to switch Xcode Developer Tools. ([#3079](https://github.com/expo/eas-cli/pull/3079) by [@rolfb](https://github.com/rolfb))
|
||||
- Skip configuring build profiles when running `eas update:configure` if no build profiles exist. ([#3084](https://github.com/expo/eas-cli/pull/3084) by [@brentvatne](https://github.com/brentvatne))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Reword how paid plans are described ([#3082](https://github.com/expo/eas-cli/pull/3082) by [@ide](https://github.com/ide))
|
||||
|
||||
## [16.13.2](https://github.com/expo/eas-cli/releases/tag/v16.13.2) - 2025-06-27
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Amend update override fields. ([#3080](https://github.com/expo/eas-cli/pull/3080) by [@quinlanj](https://github.com/quinlanj))
|
||||
|
||||
## [16.13.1](https://github.com/expo/eas-cli/releases/tag/v16.13.1) - 2025-06-25
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Fix bug in log during update:republish. ([#3067](https://github.com/expo/eas-cli/pull/3067) by [@wschurman](https://github.com/wschurman))
|
||||
- Fix update group deletion. ([#3069](https://github.com/expo/eas-cli/pull/3069) by [@wschurman](https://github.com/wschurman))
|
||||
|
||||
## [16.13.0](https://github.com/expo/eas-cli/releases/tag/v16.13.0) - 2025-06-24
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Add `--what-to-test` option to `build` command. ([#3065](https://github.com/expo/eas-cli/pull/3065) by [@vonovak](https://github.com/vonovak))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Hide `workflow:list`. ([#3060](https://github.com/expo/eas-cli/pull/3060) by [@douglowder](https://github.com/douglowder))
|
||||
- Align text of messages from `build` and `workflow:run`. ([#3061](https://github.com/expo/eas-cli/pull/3061) by [@douglowder](https://github.com/douglowder))
|
||||
|
||||
## [16.12.0](https://github.com/expo/eas-cli/releases/tag/v16.12.0) - 2025-06-20
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Add `--what-to-test` option to the `submit` command. ([#3023](https://github.com/expo/eas-cli/pull/3023) by [@vonovak](https://github.com/vonovak))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Fixed monorepo support in `workflow:run` if a project is not connected to a GitHub repository. ([#3058](https://github.com/expo/eas-cli/pull/3058) by [@sjchmiela](https://github.com/sjchmiela))
|
||||
|
||||
## [16.10.1](https://github.com/expo/eas-cli/releases/tag/v16.10.1) - 2025-06-13
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Fixed `eas build:dev` command to correctly use passed build profile. ([#3053](https://github.com/expo/eas-cli/pull/3053) by [@sebryu](https://github.com/sebryu))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Allow rolling out update on empty branch. ([#3050](https://github.com/expo/eas-cli/pull/3050) by [@wschurman](https://github.com/wschurman))
|
||||
|
||||
## [16.10.0](https://github.com/expo/eas-cli/releases/tag/v16.10.0) - 2025-06-12
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- [eas-cli] Add workflow:cancel. ([#3048](https://github.com/expo/eas-cli/pull/3048) by [@douglowder](https://github.com/douglowder))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Removed repack support. ([#3044](https://github.com/expo/eas-cli/pull/3044) by [@kudo](https://github.com/kudo))
|
||||
|
||||
## [16.9.0](https://github.com/expo/eas-cli/releases/tag/v16.9.0) - 2025-06-06
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Add `groups` option to the `submit` command. ([#2891](https://github.com/expo/eas-cli/pull/2891) by [@khamilowicz](https://github.com/khamilowicz))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- [eas-cli] Ensure workflow:runs returns most recent runs. ([#3045](https://github.com/expo/eas-cli/pull/3045) by [@douglowder](https://github.com/douglowder))
|
||||
|
||||
## [16.8.0](https://github.com/expo/eas-cli/releases/tag/v16.8.0) - 2025-06-04
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- [eas-cli] Add workflow:list and workflow:runs. ([#3030](https://github.com/expo/eas-cli/pull/3030) by [@douglowder](https://github.com/douglowder))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Change location of EAS Update host override config. ([#3042](https://github.com/expo/eas-cli/pull/3042) by [@wschurman](https://github.com/wschurman))
|
||||
|
||||
## [16.7.2](https://github.com/expo/eas-cli/releases/tag/v16.7.2) - 2025-06-03
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Add support for array `flavorDimensions` (`flavorDimensions = ['version']`). ([#3043](https://github.com/expo/eas-cli/pull/3043) by [@sjchmiela](https://github.com/sjchmiela))
|
||||
|
||||
## [16.7.1](https://github.com/expo/eas-cli/releases/tag/v16.7.1) - 2025-06-03
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Fix some unit tests by updating mocks. ([#3035](https://github.com/expo/eas-cli/pull/3035) by [@douglowder](https://github.com/douglowder))
|
||||
- Fix update republishing not including environment or git info. ([#3036](https://github.com/expo/eas-cli/pull/3036) by [@wschurman](https://github.com/wschurman))
|
||||
- Do not require `eas.json` or `package.json` files when creating new workflow runs. ([#3028](https://github.com/expo/eas-cli/pull/3028) by [@sjchmiela](https://github.com/sjchmiela))
|
||||
- Add `EAS_SKIP_CLI_VERSION_CHECK` allowing us to skip `eas-cli` version check (against `eas.json#version`). ([#3041](https://github.com/expo/eas-cli/pull/3041) by [@sjchmiela](https://github.com/sjchmiela))
|
||||
|
||||
## [16.7.0](https://github.com/expo/eas-cli/releases/tag/v16.7.0) - 2025-05-29
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Add support for manifest host and asset host overriding for EAS Update. ([#3021](https://github.com/expo/eas-cli/pull/3021) by [@wschurman](https://github.com/wschurman))
|
||||
- Add rollout flag to update:republish. ([#3029](https://github.com/expo/eas-cli/pull/3029) by [@quinlanj](https://github.com/quinlanj))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
## [16.6.2](https://github.com/expo/eas-cli/releases/tag/v16.6.2) - 2025-05-23
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
@@ -0,0 +1,359 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Monorepo Structure
|
||||
|
||||
This is a **Lerna-based monorepo** containing the EAS CLI and all supporting build libraries.
|
||||
|
||||
### CLI & Configuration Packages
|
||||
|
||||
1. **[packages/eas-cli](./packages/eas-cli/CLAUDE.md)** - The main CLI tool (published as `eas-cli`)
|
||||
|
||||
- 95+ commands organized by domain (build, submit, update, channel, credentials, etc.)
|
||||
- Built on oclif (Open CLI Framework)
|
||||
- Entry point: `packages/eas-cli/bin/run`
|
||||
|
||||
2. **[packages/eas-json](./packages/eas-json/CLAUDE.md)** - EAS configuration parser (published as `@expo/eas-json`)
|
||||
|
||||
- Validates `eas.json` configuration files using Joi schemas
|
||||
- Manages build, submit, and deploy profiles
|
||||
- Supports JSON5 via golden-fleece
|
||||
|
||||
3. **[packages/eas-build-cache-provider](./packages/eas-build-cache-provider/CLAUDE.md)** - Build cache plugin (published as `eas-build-cache-provider`)
|
||||
|
||||
- Optimizes build caching for Expo CLI
|
||||
|
||||
### Build Execution Packages
|
||||
|
||||
4. **[packages/eas-build-job](./packages/eas-build-job)** - Build job definitions (published as `@expo/eas-build-job`)
|
||||
|
||||
- Defines all data structures for build operations (Job, BuildPhase, BuildMode, Platform, Workflow)
|
||||
- Provides type definitions and validation schemas (Zod, Joi)
|
||||
- Contains BuildPhase enum that defines the traditional build pipeline stages
|
||||
- Key exports: `Job`, `BuildPhase`, `BuildMode`, `BuildTrigger`, `Workflow`, `ArchiveSource`
|
||||
- **Foundation that all other build packages depend on**
|
||||
|
||||
5. **[packages/build-tools](./packages/build-tools)** - Build execution engine (published as `@expo/build-tools`)
|
||||
|
||||
- Orchestrates all build operations through `BuildContext<T extends Job>`
|
||||
- Contains platform-specific builders: `androidBuilder()`, `iosBuilder()`, `runCustomBuildAsync()`
|
||||
- Manages build phases, artifact uploading, caching, credentials
|
||||
- Provides functions for custom builds
|
||||
- Integrates with GraphQL API
|
||||
|
||||
6. **[packages/steps](./packages/steps)** - Custom build workflow engine (published as `@expo/steps`)
|
||||
|
||||
- Framework for defining and executing custom build steps
|
||||
- Key abstractions:
|
||||
- `BuildWorkflow`: Orchestrates sequential step execution
|
||||
- `BuildStep`: Individual executable unit with inputs/outputs
|
||||
- `BuildStepGlobalContext`: Manages shared state and interpolation
|
||||
- `BuildStepContext`: Per-step execution context
|
||||
- Supports conditional execution with `if` expressions (using jsep)
|
||||
- Template interpolation: `${{ steps.step-id.outputs.outputName }}`
|
||||
- Parses build configs from YAML/JSON
|
||||
|
||||
7. **[packages/local-build-plugin](./packages/local-build-plugin)** - Local build execution (published as `eas-cli-local-build-plugin`)
|
||||
|
||||
- Allows running EAS builds locally on developer machines
|
||||
- Entry point: `packages/local-build-plugin/src/main.ts`
|
||||
- Sets `EAS_BUILD_RUNNER=local-build-plugin` environment variable
|
||||
- Reuses all build-tools logic for consistency with cloud builds
|
||||
|
||||
8. **[packages/worker](./packages/worker/CLAUDE.md)** - Turtle Worker service (private, not published)
|
||||
- Runs on EAS build VMs/pods
|
||||
- WebSocket server for communication with Turtle Launcher
|
||||
- Wraps `@expo/build-tools` to execute actual React Native builds
|
||||
|
||||
### Supporting Packages
|
||||
|
||||
9. **[packages/logger](./packages/logger)** - Bunyan-based structured logging (published as `@expo/logger`)
|
||||
|
||||
- Used by all build packages
|
||||
|
||||
10. **[packages/downloader](./packages/downloader)** - HTTP file downloading with retry logic (published as `@expo/downloader`)
|
||||
|
||||
11. **[packages/turtle-spawn](./packages/turtle-spawn)** - Child process spawning with error handling (published as `@expo/turtle-spawn`)
|
||||
|
||||
12. **[packages/template-file](./packages/template-file)** - Lodash-based template string interpolation (published as `@expo/template-file`)
|
||||
|
||||
13. **[packages/create-eas-build-function](./packages/create-eas-build-function)** - CLI scaffolding tool for custom build functions (published as `create-eas-build-function`)
|
||||
|
||||
14. **[packages/expo-cocoapods-proxy](./packages/expo-cocoapods-proxy)** - Ruby gem for CocoaPods proxy (published as `expo-cocoapods-proxy` gem)
|
||||
|
||||
> **Note**: Some packages have their own CLAUDE.md with package-specific guidance. Click the links above to view them.
|
||||
|
||||
## Common Development Commands
|
||||
|
||||
### Setup
|
||||
|
||||
```bash
|
||||
yarn install # Install all dependencies
|
||||
yarn build # Build all packages in the monorepo
|
||||
```
|
||||
|
||||
### Development
|
||||
|
||||
```bash
|
||||
yarn start # Watch mode with parallel builds across all packages
|
||||
yarn watch # Alias for start
|
||||
|
||||
# Development with relaxed TypeScript rules (allows unused variables)
|
||||
yarn start-allow-unused
|
||||
```
|
||||
|
||||
### Testing
|
||||
|
||||
```bash
|
||||
yarn lerna run test # Run all tests across packages
|
||||
yarn lerna run test -- --watch # Watch mode across packages (heavier)
|
||||
#
|
||||
# If you use `yarn test`, remember to pass args through two separators:
|
||||
# yarn test -- -- --watch
|
||||
|
||||
# Run tests for a specific package
|
||||
cd packages/eas-cli && yarn test
|
||||
cd packages/eas-json && yarn test
|
||||
cd packages/worker && yarn test
|
||||
cd packages/build-tools && yarn test
|
||||
cd packages/steps && yarn test
|
||||
```
|
||||
|
||||
### Type Checking & Linting
|
||||
|
||||
```bash
|
||||
yarn typecheck # TypeScript type check all packages
|
||||
yarn lint # ESLint across all packages
|
||||
```
|
||||
|
||||
### Running EAS CLI Locally
|
||||
|
||||
```bash
|
||||
# From repository root:
|
||||
yarn eas <command> # Run via root package script
|
||||
|
||||
# Or use the binary directly:
|
||||
packages/eas-cli/bin/run <command>
|
||||
|
||||
# Recommended: Create an alias for development
|
||||
alias easd="$(pwd)/packages/eas-cli/bin/run"
|
||||
easd build --help
|
||||
```
|
||||
|
||||
### Local Build Testing
|
||||
|
||||
Set up environment variables for testing local builds:
|
||||
|
||||
```bash
|
||||
export EAS_LOCAL_BUILD_PLUGIN_PATH=$HOME/expo/eas-cli/bin/eas-cli-local-build-plugin
|
||||
export EAS_LOCAL_BUILD_WORKINGDIR=$HOME/expo/eas-build-workingdir
|
||||
export EAS_LOCAL_BUILD_SKIP_CLEANUP=1
|
||||
export EAS_LOCAL_BUILD_ARTIFACTS_DIR=$HOME/expo/eas-build-workingdir/results
|
||||
|
||||
# Then run build with --local flag in eas-cli
|
||||
eas build --local
|
||||
```
|
||||
|
||||
## Build System
|
||||
|
||||
Each package has independent TypeScript compilation:
|
||||
|
||||
- `eas-cli`: `src/` → `build/`
|
||||
- `eas-json`: `src/` → `build/`
|
||||
- `eas-build-cache-provider`: `src/` → `build/`
|
||||
- `worker`: `src/` → `dist/`
|
||||
- `build-tools`: `src/` → `dist/`
|
||||
- `eas-build-job`: `src/` → `dist/`
|
||||
- `steps`: `src/` → `dist/` (CommonJS only)
|
||||
- `local-build-plugin`: `src/` → `dist/`
|
||||
- Other packages: `src/` → `dist/`
|
||||
|
||||
TypeScript configs:
|
||||
|
||||
- `tsconfig.json` - Base configuration (extends @tsconfig/node20)
|
||||
- `tsconfig.build.json` - Production builds
|
||||
- `tsconfig.allowUnused.json` - Development with relaxed rules
|
||||
|
||||
## Key Architectural Patterns
|
||||
|
||||
### Build Phases
|
||||
|
||||
Most traditional build operations are wrapped in phases for tracking:
|
||||
|
||||
```typescript
|
||||
await ctx.runBuildPhase(BuildPhase.INSTALL_DEPENDENCIES, async () => {
|
||||
// Phase logic here
|
||||
});
|
||||
```
|
||||
|
||||
Phases can be marked as skipped, warning, or failed for granular reporting.
|
||||
|
||||
### Context Objects
|
||||
|
||||
- **BuildContext** (`build-tools`): For traditional builds, wraps Job, manages phases/artifacts/caching
|
||||
- **CustomBuildContext** (`build-tools`): Implements `ExternalBuildContextProvider`, bridges BuildContext to steps framework, used in custom builds and generic jobs
|
||||
- **BuildStepGlobalContext** (`steps`): Manages step outputs, interpolation, shared state
|
||||
- **BuildStepContext** (`steps`): Per-step context with working directory and logger
|
||||
|
||||
### Custom Build Steps
|
||||
|
||||
Steps are defined with:
|
||||
|
||||
- `id`: Unique identifier
|
||||
- `name`: Display name
|
||||
- `run`: Command or function reference
|
||||
- `if`: Optional condition (`${{ always() }}`, `${{ success() }}`, etc.)
|
||||
- `inputs`: Key-value inputs to the step
|
||||
- `outputs`: Named outputs accessible to later steps
|
||||
|
||||
Built-in step functions are in `packages/build-tools/src/steps/functions/`
|
||||
|
||||
### Conditional Execution
|
||||
|
||||
Uses jsep for expression evaluation:
|
||||
|
||||
```yaml
|
||||
if: ${{ steps.previous_step.outputs.success == 'true' && env.ENVIRONMENT == 'production' }}
|
||||
```
|
||||
|
||||
### Artifact Management
|
||||
|
||||
Artifacts tracked as `ArtifactToUpload`:
|
||||
|
||||
- Managed artifacts (APK, IPA, AAB) with specific handling
|
||||
- Generic artifacts for any file
|
||||
- Upload via `ctx.uploadArtifact()` or `upload-artifact` step
|
||||
|
||||
## Package Interdependencies
|
||||
|
||||
```
|
||||
eas-cli → @expo/eas-json
|
||||
→ @expo/build-tools → @expo/eas-build-job
|
||||
→ @expo/steps → @expo/eas-build-job
|
||||
→ @expo/logger
|
||||
→ @expo/turtle-spawn
|
||||
→ @expo/downloader
|
||||
→ @expo/template-file
|
||||
|
||||
local-build-plugin → @expo/build-tools → @expo/eas-build-job
|
||||
→ @expo/turtle-spawn
|
||||
|
||||
worker → @expo/build-tools
|
||||
```
|
||||
|
||||
Most packages depend on `@expo/eas-build-job` as the source of truth for types.
|
||||
|
||||
## Testing Architecture
|
||||
|
||||
- **Framework**: Jest with per-package configuration
|
||||
- **Shared base config**: `jest/jest.shared.config.ts`
|
||||
- **Test locations**: `__tests__/` directories alongside source files
|
||||
- **Mocking**:
|
||||
- File system: `memfs`
|
||||
- HTTP: `nock`
|
||||
- Utilities: `ts-mockito`, `mockdate`
|
||||
|
||||
Run a single test file:
|
||||
|
||||
```bash
|
||||
cd packages/eas-cli && yarn test <path-to-test-file>
|
||||
# Example:
|
||||
cd packages/eas-cli && yarn test src/project/__tests__/projectUtils-test.ts
|
||||
```
|
||||
|
||||
## Common Development Scenarios
|
||||
|
||||
### Adding a Built-in Step Function
|
||||
|
||||
1. Create file in `packages/build-tools/src/steps/functions/yourFunction.ts`
|
||||
2. Export `createYourFunctionBuildFunction()` following existing patterns
|
||||
3. Add to `getEasFunctions()` in `packages/build-tools/src/steps/functions/easFunctions.ts`
|
||||
4. Function receives `BuildStepContext` and input/output maps
|
||||
|
||||
### Adding Error Detection
|
||||
|
||||
1. Add pattern detection in `packages/build-tools/src/buildErrors/detectError.ts`
|
||||
2. Implement resolver for better error messages
|
||||
3. Helps users understand and fix build failures
|
||||
|
||||
### Working with Platform-Specific Builders
|
||||
|
||||
- **Android builder** (`packages/build-tools/src/builders/android.ts`): Gradle-based, handles APK/AAB generation, also see `functionGroups/build.ts`
|
||||
- **iOS builder** (`packages/build-tools/src/builders/ios.ts`): Fastlane/Xcode-based, handles IPA generation, also see `functionGroups/build.ts`
|
||||
- Both use `runBuilderWithHooksAsync()` which runs build result hooks (on-success, on-error, on-complete) from package.json
|
||||
|
||||
### Introducing Breaking Changes to Job API (@expo/eas-build-job)
|
||||
|
||||
If you want to introduce breaking changes to the `@expo/eas-build-job` package, contact one of the CODEOWNERS to coordinate changes with EAS build servers and GraphQL API. Describe what changes you want to make and why. After everything is deployed to production, you can introduce a PR that relies on the new implementation.
|
||||
|
||||
## Environment Variables
|
||||
|
||||
```bash
|
||||
# API Configuration
|
||||
export EXPO_STAGING=1 # Use staging API (https://staging-api.expo.dev)
|
||||
export EXPO_LOCAL=1 # Use local API (http://127.0.0.1:3000)
|
||||
|
||||
# Development
|
||||
export EAS_NO_VCS=1 # Disable version control checks
|
||||
export EXPO_DEBUG=1 # Enable debug logging
|
||||
```
|
||||
|
||||
## Code Style & Conventions
|
||||
|
||||
### Validation
|
||||
|
||||
All changes should be validated with TypeScript and the linter before committing:
|
||||
|
||||
```bash
|
||||
yarn typecheck # Validate TypeScript types
|
||||
yarn lint # Run ESLint
|
||||
```
|
||||
|
||||
### Logging
|
||||
|
||||
Use the `Log` module from `@expo/logger`:
|
||||
|
||||
```typescript
|
||||
import { Log } from '@expo/logger';
|
||||
|
||||
Log.log('Info message');
|
||||
Log.warn('Warning message');
|
||||
Log.error('Error message');
|
||||
```
|
||||
|
||||
### Pull Request conventions
|
||||
|
||||
When creating pull requests, make sure to adhere to [PULL_REQUEST_TEMPLATE](./.github/PULL_REQUEST_TEMPLATE).
|
||||
|
||||
### Commit Messages
|
||||
|
||||
- When possible, prepend commit messages with `[PACKAGE-BEING-CHANGED]`, e.g. `[steps] Add new step function`
|
||||
- Do not use prefixes like `chore:` and `feat:`
|
||||
- Commit messages should be concise. Only complex changes should be longer than one line
|
||||
- Commit changes in logical groups
|
||||
|
||||
## Release Process
|
||||
|
||||
1. Update `CHANGELOG.md` in the appropriate package
|
||||
2. Version bump is automated based on changelog:
|
||||
- Breaking changes → MAJOR
|
||||
- New features → MINOR
|
||||
- Otherwise → PATCH
|
||||
3. GitHub Actions workflow handles release automation
|
||||
4. Notifications sent to Slack #eas-cli channel
|
||||
|
||||
## Licensing
|
||||
|
||||
This repository contains packages under different licenses:
|
||||
|
||||
- **MIT License**: `eas-cli`, `@expo/eas-json`, `@expo/eas-build-job`, `eas-build-cache-provider`
|
||||
- **BUSL-1.1 (Business Source License)**: `@expo/build-tools`, `@expo/steps`, `@expo/logger`, `@expo/downloader`, `@expo/turtle-spawn`, `@expo/template-file`, `eas-cli-local-build-plugin`
|
||||
|
||||
See `LICENSE` (MIT) and `LICENSE-BUSL` (BUSL-1.1) for details.
|
||||
|
||||
## Important Notes
|
||||
|
||||
- **Node Version**: Requires Node.js (managed via Mise)
|
||||
- **Package Manager**: Uses Yarn 1.22.21
|
||||
- **Compilation Target**: CommonJS with Node resolution
|
||||
+16
-7
@@ -1,12 +1,21 @@
|
||||
# Development
|
||||
|
||||
Set up an alias for the EAS CLI so you can try it in projects all around your computer. The project is compiled on the fly so you don't need to run a build (watch) command.
|
||||
## Getting Started
|
||||
|
||||
> The only exception is when you just cloned the repository or there have been changes in `packages/eas-json`. In that case, you'll have to run `yarn build` in the root.
|
||||
If this is your first time running `eas-cli` locally, make sure you install and build the packages. In the project root, run:
|
||||
|
||||
```sh
|
||||
yarn install
|
||||
yarn build
|
||||
```
|
||||
|
||||
From there, set up an alias for the EAS CLI so you can try it in projects all around your computer. The project is compiled on the fly so you don't need to run a build (watch) command.
|
||||
|
||||
> The only exception is if there have been changes in `packages/eas-json`. In that case, you'll have to run `yarn build` in the root.
|
||||
|
||||
Open your `.zshrc` or other config file and add:
|
||||
|
||||
```
|
||||
```sh
|
||||
alias easd="/PATH/TO/eas-cli/bin/run"
|
||||
```
|
||||
|
||||
@@ -14,7 +23,7 @@ Then use it with `easd` like `easd build`.
|
||||
|
||||
If you're making changes to `packages/eas-json` or prefer/need to work with production code, start the build command in watch mode with:
|
||||
|
||||
```
|
||||
```sh
|
||||
yarn start
|
||||
```
|
||||
|
||||
@@ -33,7 +42,7 @@ If it is easier for you to work with the code locally with compiler options set
|
||||
|
||||
you can consider using:
|
||||
|
||||
```
|
||||
```sh
|
||||
yarn start-allow-unused
|
||||
```
|
||||
|
||||
@@ -56,11 +65,11 @@ For development against staging API:
|
||||
|
||||
## Working on local builds (`eas build --local`)
|
||||
|
||||
See https://github.com/expo/eas-build/blob/main/DEVELOPMENT.md for how to set up your environment when making changes to [`eas-cli-local-build-plugin`](https://github.com/expo/eas-build/tree/main/packages/local-build-plugin) and/or [`build-tools`](https://github.com/expo/eas-build/tree/main/packages/build-tools).
|
||||
See [`CLAUDE.md`](./CLAUDE.md) for how to set up your environment when making changes to [`eas-cli-local-build-plugin`](./packages/local-build-plugin) and/or [`build-tools`](./packages/build-tools).
|
||||
|
||||
## Testing
|
||||
|
||||
Run `yarn test` either in the repository root or in a package directory that you're working on.
|
||||
From the repository root, run `yarn lerna run test`. For a single package, run `yarn test` in that package directory.
|
||||
|
||||
## Releasing
|
||||
|
||||
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
Business Source License 1.1
|
||||
|
||||
Parameters
|
||||
|
||||
Licensor: 650 Industries, Inc.
|
||||
Licensed Work: EAS Build
|
||||
The Licensed Work is (c) 2021 650 Industries, Inc.
|
||||
Additional Use Grant: You may make use of the Licensed Work, provided that you do
|
||||
not use the Licensed Work for commercial offerings such as
|
||||
a CI/CD service or application build service that allows
|
||||
third parties (other than your employees and contractors)
|
||||
to access the functionality of and directly benefit from the
|
||||
functionality of the Licensed Work.
|
||||
|
||||
Change Date: 2028-04-01
|
||||
|
||||
Change License: MIT
|
||||
|
||||
For information about alternative licensing arrangements for the Software,
|
||||
please visit: https://expo.dev/pricing/
|
||||
|
||||
Notice
|
||||
|
||||
The Business Source License (this document, or the "License") is not an Open
|
||||
Source license. However, the Licensed Work will eventually be made available
|
||||
under an Open Source License, as stated in this License.
|
||||
|
||||
License text copyright (c) 2017 MariaDB Corporation Ab, All Rights Reserved.
|
||||
"Business Source License" is a trademark of MariaDB Corporation Ab.
|
||||
|
||||
-----------------------------------------------------------------------------
|
||||
|
||||
Business Source License 1.1
|
||||
|
||||
Terms
|
||||
|
||||
The Licensor hereby grants you the right to copy, modify, create derivative
|
||||
works, redistribute, and make non-production use of the Licensed Work. The
|
||||
Licensor may make an Additional Use Grant, above, permitting limited
|
||||
production use.
|
||||
|
||||
Effective on the Change Date, or the fourth anniversary of the first publicly
|
||||
available distribution of a specific version of the Licensed Work under this
|
||||
License, whichever comes first, the Licensor hereby grants you rights under
|
||||
the terms of the Change License, and the rights granted in the paragraph
|
||||
above terminate.
|
||||
|
||||
If your use of the Licensed Work does not comply with the requirements
|
||||
currently in effect as described in this License, you must purchase a
|
||||
commercial license from the Licensor, its affiliated entities, or authorized
|
||||
resellers, or you must refrain from using the Licensed Work.
|
||||
|
||||
All copies of the original and modified Licensed Work, and derivative works
|
||||
of the Licensed Work, are subject to this License. This License applies
|
||||
separately for each version of the Licensed Work and the Change Date may vary
|
||||
for each version of the Licensed Work released by Licensor.
|
||||
|
||||
You must conspicuously display this License on each original or modified copy
|
||||
of the Licensed Work. If you receive the Licensed Work in original or
|
||||
modified form from a third party, the terms and conditions set forth in this
|
||||
License apply to your use of that work.
|
||||
|
||||
Any use of the Licensed Work in violation of this License will automatically
|
||||
terminate your rights under this License for the current and all other
|
||||
versions of the Licensed Work.
|
||||
|
||||
This License does not grant you any right in any trademark or logo of
|
||||
Licensor or its affiliates (provided that you may use a trademark or logo of
|
||||
Licensor as expressly required by this License).
|
||||
|
||||
TO THE EXTENT PERMITTED BY APPLICABLE LAW, THE LICENSED WORK IS PROVIDED ON
|
||||
AN "AS IS" BASIS. LICENSOR HEREBY DISCLAIMS ALL WARRANTIES AND CONDITIONS,
|
||||
EXPRESS OR IMPLIED, INCLUDING (WITHOUT LIMITATION) WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, AND
|
||||
TITLE.
|
||||
|
||||
MariaDB hereby grants you permission to use this License’s text to license
|
||||
your works, and to refer to it using the trademark "Business Source License",
|
||||
as long as you comply with the Covenants of Licensor below.
|
||||
|
||||
Covenants of Licensor
|
||||
|
||||
In consideration of the right to use this License’s text and the "Business
|
||||
Source License" name and trademark, Licensor covenants to MariaDB, and to all
|
||||
other recipients of the licensed work to be provided by Licensor:
|
||||
|
||||
1. To specify as the Change License the GPL Version 2.0 or any later version,
|
||||
or a license that is compatible with GPL Version 2.0 or a later version,
|
||||
where "compatible" means that software provided under the Change License can
|
||||
be included in a program with software provided under GPL Version 2.0 or a
|
||||
later version. Licensor may specify additional Change Licenses without
|
||||
limitation.
|
||||
|
||||
2. To either: (a) specify an additional grant of rights to use that does not
|
||||
impose any additional restriction on the right granted in this License, as
|
||||
the Additional Use Grant; or (b) insert the text "None".
|
||||
|
||||
3. To specify a Change Date.
|
||||
|
||||
4. Not to modify this License in any other way.
|
||||
@@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2020-present 650 Industries, Inc. (aka Expo)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
# Releasing EAS CLI
|
||||
|
||||
1. Run `yarn release` in the repository root folder. The next version is chosen automatically based on the changelog entries. If you want to use different version, pass the version string as a positional argument to the command, e.g. `yarn release 1.2.3`.
|
||||
1. Invoke the GitHub workflow ["Manually trigger a release"](https://github.com/expo/eas-cli/actions/workflows/trigger-release.yml). The next version is chosen automatically based on the changelog entries. If you want to use different version, pass the version string as an input to the workflow, in semver format, e.g. "1.2.3"
|
||||
2. That's it! GitHub Actions is going to take care of the rest. Watch the #eas-cli Slack channel for a successful release notification.
|
||||
|
||||
## Choosing the next version
|
||||
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
../packages/local-build-plugin/bin/run
|
||||
@@ -1,15 +0,0 @@
|
||||
import type { Config } from 'jest';
|
||||
|
||||
// eslint-disable-next-line import/no-relative-packages
|
||||
import easCliConfig from './packages/eas-cli/jest.config';
|
||||
// eslint-disable-next-line import/no-relative-packages
|
||||
import easJsonConfig from './packages/eas-json/jest.config';
|
||||
|
||||
const config: Config = {
|
||||
projects: [easCliConfig, easJsonConfig],
|
||||
testPathIgnorePatterns: ['.*'],
|
||||
watchPlugins: ['jest-watch-typeahead/filename', 'jest-watch-typeahead/testname'],
|
||||
collectCoverageFrom: ['<rootDir>/src/**/*.{ts,tsx,js,jsx}'],
|
||||
};
|
||||
|
||||
export default config;
|
||||
@@ -1,9 +0,0 @@
|
||||
import type { Config } from 'jest';
|
||||
|
||||
const config: Config = {
|
||||
preset: 'ts-jest',
|
||||
testEnvironment: 'node',
|
||||
testRegex: '/__tests__/.*(test|spec)\\.[jt]sx?$',
|
||||
};
|
||||
|
||||
export default config;
|
||||
@@ -0,0 +1,8 @@
|
||||
import type { Config } from '@jest/types';
|
||||
|
||||
const config: Config.InitialOptions = {
|
||||
preset: 'ts-jest',
|
||||
testEnvironment: 'node',
|
||||
};
|
||||
|
||||
export default config;
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"npmClient": "yarn",
|
||||
"version": "16.7.0",
|
||||
"version": "17.0.0",
|
||||
"$schema": "node_modules/lerna/schemas/lerna-schema.json"
|
||||
}
|
||||
+4
-8
@@ -16,26 +16,22 @@
|
||||
"lint": "eslint .",
|
||||
"lint-changelog": "./scripts/bin/run lint-changelog",
|
||||
"release": "lerna run rebuild && ./scripts/bin/run release",
|
||||
"test": "jest",
|
||||
"test": "lerna run test",
|
||||
"clean": "lerna run clean && rm -rf node_modules coverage"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jest": "29.5.11",
|
||||
"@types/node": "20.11.0",
|
||||
"@types/node": "20.14.8",
|
||||
"eslint": "8.56.0",
|
||||
"eslint-config-universe": "13.0.0",
|
||||
"eslint-plugin-async-protect": "3.1.0",
|
||||
"eslint-plugin-node": "11.1.0",
|
||||
"jest": "29.7.0",
|
||||
"jest-watch-typeahead": "2.2.2",
|
||||
"lerna": "7.4.2",
|
||||
"lerna": "9.0.3",
|
||||
"oclif": "^3.9.0",
|
||||
"prettier": "3.1.1",
|
||||
"ts-jest": "29.1.1",
|
||||
"typescript": "5.3.3"
|
||||
},
|
||||
"volta": {
|
||||
"node": "20.11.0",
|
||||
"yarn": "1.22.21"
|
||||
"typescript": "5.5.4"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"extends": "../../.eslintrc.js",
|
||||
"rules": {
|
||||
"async-protect/async-suffix": "off",
|
||||
"no-underscore-dangle": "off",
|
||||
"node/no-sync": "off",
|
||||
"no-console": "off",
|
||||
"no-void": "off",
|
||||
"no-restricted-imports": "off"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
node_modules/
|
||||
dist/
|
||||
|
||||
.secrets*
|
||||
!.secrets.example
|
||||
!.secrets.test
|
||||
|
||||
src/graphql-env.d.ts
|
||||
schema.graphql
|
||||
@@ -0,0 +1,7 @@
|
||||
# @expo/build-tools
|
||||
|
||||
`@expo/build-tools` is the core library for the EAS Build service. It implements the build process for React Native projects and for managed Expo applications.
|
||||
|
||||
## Repository
|
||||
|
||||
https://github.com/expo/eas-cli/tree/main/packages/build-tools
|
||||
Executable
+23
@@ -0,0 +1,23 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -eo pipefail
|
||||
|
||||
NAME=$1
|
||||
VALUE=$2
|
||||
|
||||
if [[ -z "$__EAS_BUILD_ENVS_DIR" ]]; then
|
||||
echo "Set __EAS_BUILD_ENVS_DIR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -z "$NAME" || -z "$VALUE" ]]; then
|
||||
echo "Usage: set-env NAME VALUE"
|
||||
exit 2
|
||||
fi
|
||||
|
||||
if [[ "$NAME" == *"="* ]]; then
|
||||
echo "Environment name can't include ="
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo -n $VALUE > $__EAS_BUILD_ENVS_DIR/$NAME
|
||||
@@ -0,0 +1,14 @@
|
||||
import type { Config } from '@jest/types';
|
||||
|
||||
// eslint-disable-next-line import/no-relative-packages
|
||||
import baseConfig from '../../../jest/jest.shared.config';
|
||||
|
||||
const config: Config.InitialOptions = {
|
||||
...baseConfig,
|
||||
rootDir: '../src',
|
||||
testMatch: ['**/__integration-tests__/*.test.ts'],
|
||||
clearMocks: true,
|
||||
setupFilesAfterEnv: ['<rootDir>/../jest/setup-tests.ts'],
|
||||
};
|
||||
|
||||
export default config;
|
||||
@@ -0,0 +1,20 @@
|
||||
import { fs, vol } from 'memfs';
|
||||
import os from 'node:os';
|
||||
|
||||
if (process.env.NODE_ENV !== 'test') {
|
||||
throw new Error("NODE_ENV environment variable must be set to 'test'.");
|
||||
}
|
||||
|
||||
// Always mock:
|
||||
jest.mock('fs');
|
||||
jest.mock('node:fs', () => fs);
|
||||
jest.mock('fs/promises');
|
||||
jest.mock('node:fs/promises', () => fs.promises);
|
||||
jest.mock('node-fetch');
|
||||
|
||||
beforeEach(() => {
|
||||
vol.reset();
|
||||
vol.fromNestedJSON({
|
||||
[os.tmpdir()]: {},
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,17 @@
|
||||
import type { Config } from '@jest/types';
|
||||
|
||||
// eslint-disable-next-line import/no-relative-packages
|
||||
import baseConfig from '../../../jest/jest.shared.config';
|
||||
|
||||
const config: Config.InitialOptions = {
|
||||
...baseConfig,
|
||||
rootDir: '../src',
|
||||
testMatch: [
|
||||
'**/__tests__/*.test.ts',
|
||||
...(process.platform === 'darwin' ? ['**/__tests__/*.test.ios.ts'] : []),
|
||||
],
|
||||
clearMocks: true,
|
||||
setupFilesAfterEnv: ['<rootDir>/../jest/setup-tests.ts'],
|
||||
};
|
||||
|
||||
export default config;
|
||||
@@ -0,0 +1,97 @@
|
||||
{
|
||||
"name": "@expo/build-tools",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/expo/eas-cli.git",
|
||||
"directory": "packages/build-tools"
|
||||
},
|
||||
"version": "17.0.0",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"files": [
|
||||
"dist",
|
||||
"resources",
|
||||
"templates",
|
||||
"bin"
|
||||
],
|
||||
"scripts": {
|
||||
"start": "yarn watch",
|
||||
"prewatch": "yarn gql",
|
||||
"watch": "tsc --watch --preserveWatchOutput",
|
||||
"prebuild": "yarn gql",
|
||||
"build": "tsc",
|
||||
"typecheck": "tsc",
|
||||
"prepack": "rm -rf dist && tsc -p tsconfig.build.json",
|
||||
"jest-unit": "jest --config jest/unit-config.ts",
|
||||
"jest-integration": "jest --config jest/integration-config.ts",
|
||||
"jest-unit-watch": "jest --config jest/unit-config.ts --watch",
|
||||
"jest-integration-watch": "jest --config jest/integration-config.ts --watch",
|
||||
"clean": "rm -rf node_modules dist coverage *.tsbuildinfo",
|
||||
"gql": "gql.tada generate-schema ${API_SERVER_URL:-https://api.expo.dev}/graphql --output ./schema.graphql && gql.tada generate-output",
|
||||
"gql:local": "API_SERVER_URL=http://api.expo.test yarn gql",
|
||||
"test": "yarn jest-unit"
|
||||
},
|
||||
"author": "Expo <[email protected]>",
|
||||
"bugs": "https://github.com/expo/eas-cli/issues",
|
||||
"license": "BUSL-1.1",
|
||||
"dependencies": {
|
||||
"@expo/config": "10.0.6",
|
||||
"@expo/config-plugins": "9.0.12",
|
||||
"@expo/downloader": "17.0.0",
|
||||
"@expo/eas-build-job": "17.0.0",
|
||||
"@expo/env": "^0.4.0",
|
||||
"@expo/logger": "17.0.0",
|
||||
"@expo/package-manager": "1.9.10",
|
||||
"@expo/plist": "^0.2.0",
|
||||
"@expo/results": "^1.0.0",
|
||||
"@expo/spawn-async": "1.7.2",
|
||||
"@expo/steps": "17.0.0",
|
||||
"@expo/template-file": "17.0.0",
|
||||
"@expo/turtle-spawn": "17.0.0",
|
||||
"@expo/xcpretty": "^4.3.1",
|
||||
"@google-cloud/storage": "^7.11.2",
|
||||
"@urql/core": "^6.0.1",
|
||||
"fast-glob": "^3.3.2",
|
||||
"fs-extra": "^11.2.0",
|
||||
"gql.tada": "^1.8.13",
|
||||
"joi": "^17.13.1",
|
||||
"jose": "5.10.0",
|
||||
"lodash": "^4.17.21",
|
||||
"node-fetch": "^2.7.0",
|
||||
"node-forge": "^1.3.1",
|
||||
"nullthrows": "^1.1.1",
|
||||
"plist": "^3.1.0",
|
||||
"promise-limit": "^2.7.0",
|
||||
"promise-retry": "^2.0.1",
|
||||
"resolve-from": "^5.0.0",
|
||||
"retry": "^0.13.1",
|
||||
"semver": "^7.6.2",
|
||||
"tar": "7.5.7",
|
||||
"uuid": "9.0.1",
|
||||
"yaml": "^2.8.1",
|
||||
"zod": "^4.3.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@expo/repack-app": "~0.2.5",
|
||||
"@types/fs-extra": "^11.0.4",
|
||||
"@types/jest": "^29.5.12",
|
||||
"@types/lodash": "^4.17.4",
|
||||
"@types/node": "20.14.2",
|
||||
"@types/node-fetch": "^2.6.11",
|
||||
"@types/node-forge": "^1.3.11",
|
||||
"@types/plist": "^3.0.5",
|
||||
"@types/promise-retry": "^1.1.6",
|
||||
"@types/retry": "^0.12.5",
|
||||
"@types/semver": "^7.5.8",
|
||||
"@types/uuid": "^9.0.8",
|
||||
"bunyan": "^1.8.15",
|
||||
"jest": "^29.7.0",
|
||||
"memfs": "^4.17.1",
|
||||
"nock": "13.4.0",
|
||||
"ts-jest": "^29.1.4",
|
||||
"ts-mockito": "^2.6.1",
|
||||
"tslib": "^2.6.3",
|
||||
"typescript": "^5.5.4",
|
||||
"uuid": "^9.0.1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import bunyan from 'bunyan';
|
||||
|
||||
export function createLogger(): bunyan {
|
||||
const logger = {
|
||||
info: () => {},
|
||||
debug: () => {},
|
||||
error: () => {},
|
||||
warn: () => {},
|
||||
child: _fields => logger,
|
||||
} as bunyan;
|
||||
return logger;
|
||||
}
|
||||
|
||||
export enum LoggerLevel {
|
||||
TRACE = 'trace',
|
||||
DEBUG = 'debug',
|
||||
INFO = 'info',
|
||||
WARN = 'warn',
|
||||
ERROR = 'error',
|
||||
FATAL = 'fatal',
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { fs } from 'memfs';
|
||||
|
||||
const fsRealpath = fs.realpath;
|
||||
(fsRealpath as any).native = fsRealpath;
|
||||
|
||||
const fsRm = (
|
||||
path: string,
|
||||
options: object,
|
||||
callback: (err: NodeJS.ErrnoException | null) => void
|
||||
): void => {
|
||||
fs.promises
|
||||
.rm(path, options)
|
||||
.then(() => {
|
||||
callback(null);
|
||||
})
|
||||
.catch(err => {
|
||||
callback(err);
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = { ...fs, realpath: fsRealpath, rm: fsRm };
|
||||
@@ -0,0 +1,10 @@
|
||||
import { fs } from 'memfs';
|
||||
|
||||
const fsRealpath = fs.realpath;
|
||||
(fsRealpath as any).native = fsRealpath;
|
||||
|
||||
const fsRm = (path: string, options: object): Promise<void> => {
|
||||
return fs.promises.rm(path, options);
|
||||
};
|
||||
|
||||
module.exports = { ...fs.promises, realpath: fsRealpath, rm: fsRm };
|
||||
@@ -0,0 +1,119 @@
|
||||
import { BuildTrigger, Job, Metadata } from '@expo/eas-build-job';
|
||||
import { randomUUID } from 'crypto';
|
||||
import { vol } from 'memfs';
|
||||
|
||||
import { createMockLogger } from './utils/logger';
|
||||
import { BuildContext } from '../context';
|
||||
|
||||
jest.mock('fs');
|
||||
jest.mock('fs-extra');
|
||||
|
||||
describe('BuildContext', () => {
|
||||
it('should merge secrets', async () => {
|
||||
const robotAccessToken = randomUUID();
|
||||
await vol.promises.mkdir('/workingdir/eas-environment-secrets/', { recursive: true });
|
||||
|
||||
const ctx = new BuildContext(
|
||||
{
|
||||
triggeredBy: BuildTrigger.GIT_BASED_INTEGRATION,
|
||||
secrets: {
|
||||
robotAccessToken,
|
||||
environmentSecrets: [
|
||||
{
|
||||
name: 'TEST_SECRET',
|
||||
value: 'test-secret-value',
|
||||
},
|
||||
],
|
||||
},
|
||||
} as Job,
|
||||
{
|
||||
env: {
|
||||
__API_SERVER_URL: 'http://api.expo.test',
|
||||
},
|
||||
workingdir: '/workingdir',
|
||||
logger: createMockLogger(),
|
||||
logBuffer: { getLogs: () => [], getPhaseLogs: () => [] },
|
||||
uploadArtifact: jest.fn(),
|
||||
}
|
||||
);
|
||||
|
||||
ctx.updateJobInformation({} as Job, {} as Metadata);
|
||||
|
||||
expect(ctx.job.secrets).toEqual({
|
||||
robotAccessToken,
|
||||
environmentSecrets: [
|
||||
{
|
||||
name: 'TEST_SECRET',
|
||||
value: 'test-secret-value',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const newRobotAccessToken = randomUUID();
|
||||
ctx.updateJobInformation(
|
||||
{
|
||||
secrets: {
|
||||
robotAccessToken: newRobotAccessToken,
|
||||
environmentSecrets: [
|
||||
{
|
||||
name: 'TEST_SECRET',
|
||||
value: 'new-test-secret-value',
|
||||
},
|
||||
{
|
||||
name: 'TEST_SECRET_2',
|
||||
value: 'test-secret-value-2',
|
||||
},
|
||||
],
|
||||
},
|
||||
} as Job,
|
||||
{} as Metadata
|
||||
);
|
||||
|
||||
expect(ctx.job.secrets).toEqual({
|
||||
robotAccessToken: newRobotAccessToken,
|
||||
environmentSecrets: [
|
||||
{ name: 'TEST_SECRET', value: 'test-secret-value' },
|
||||
{ name: 'TEST_SECRET', value: 'new-test-secret-value' },
|
||||
{ name: 'TEST_SECRET_2', value: 'test-secret-value-2' },
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('should not lose workflowInterpolationContext', async () => {
|
||||
const robotAccessToken = randomUUID();
|
||||
await vol.promises.mkdir('/workingdir/eas-environment-secrets/', { recursive: true });
|
||||
|
||||
const ctx = new BuildContext(
|
||||
{
|
||||
triggeredBy: BuildTrigger.GIT_BASED_INTEGRATION,
|
||||
secrets: {
|
||||
robotAccessToken,
|
||||
environmentSecrets: [
|
||||
{
|
||||
name: 'TEST_SECRET',
|
||||
value: 'test-secret-value',
|
||||
},
|
||||
],
|
||||
},
|
||||
workflowInterpolationContext: {
|
||||
foo: 'bar',
|
||||
} as any,
|
||||
} as Job,
|
||||
{
|
||||
env: {
|
||||
__API_SERVER_URL: 'http://api.expo.test',
|
||||
},
|
||||
workingdir: '/workingdir',
|
||||
logger: createMockLogger(),
|
||||
logBuffer: { getLogs: () => [], getPhaseLogs: () => [] },
|
||||
uploadArtifact: jest.fn(),
|
||||
}
|
||||
);
|
||||
|
||||
ctx.updateJobInformation({} as Job, {} as Metadata);
|
||||
|
||||
expect(ctx.job.workflowInterpolationContext).toEqual({
|
||||
foo: 'bar',
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,37 @@
|
||||
import { BuildTrigger, Ios, Metadata } from '@expo/eas-build-job';
|
||||
|
||||
import { createTestIosJob } from './utils/job';
|
||||
import { createMockLogger } from './utils/logger';
|
||||
import { BuildContext } from '../context';
|
||||
import { CustomBuildContext } from '../customBuildContext';
|
||||
|
||||
describe(CustomBuildContext, () => {
|
||||
it('should not lose workflowInterpolationContext', () => {
|
||||
const contextUploadArtifact = jest.fn();
|
||||
const ctx = new BuildContext(
|
||||
createTestIosJob({
|
||||
triggeredBy: BuildTrigger.GIT_BASED_INTEGRATION,
|
||||
workflowInterpolationContext: {
|
||||
foo: 'bar',
|
||||
} as unknown as Ios.Job['workflowInterpolationContext'],
|
||||
}),
|
||||
{
|
||||
env: {
|
||||
__API_SERVER_URL: 'http://api.expo.test',
|
||||
},
|
||||
logBuffer: { getLogs: () => [], getPhaseLogs: () => [] },
|
||||
logger: createMockLogger(),
|
||||
uploadArtifact: contextUploadArtifact,
|
||||
workingdir: '',
|
||||
}
|
||||
);
|
||||
const customContext = new CustomBuildContext(ctx);
|
||||
expect(customContext.job.workflowInterpolationContext).toStrictEqual({
|
||||
foo: 'bar',
|
||||
});
|
||||
customContext.updateJobInformation({} as Ios.Job, {} as Metadata);
|
||||
expect(customContext.job.workflowInterpolationContext).toStrictEqual({
|
||||
foo: 'bar',
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,100 @@
|
||||
import { bunyan } from '@expo/logger';
|
||||
import {
|
||||
BuildRuntimePlatform,
|
||||
BuildStepContext,
|
||||
BuildStepEnv,
|
||||
BuildStepGlobalContext,
|
||||
ExternalBuildContextProvider,
|
||||
} from '@expo/steps';
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { createMockLogger } from './logger';
|
||||
|
||||
export class MockContextProvider implements ExternalBuildContextProvider {
|
||||
private _env: BuildStepEnv = {};
|
||||
|
||||
constructor(
|
||||
public readonly logger: bunyan,
|
||||
public readonly runtimePlatform: BuildRuntimePlatform,
|
||||
public readonly projectSourceDirectory: string,
|
||||
public readonly projectTargetDirectory: string,
|
||||
public readonly defaultWorkingDirectory: string,
|
||||
public readonly buildLogsDirectory: string,
|
||||
public readonly staticContextContent: Record<string, any> = {}
|
||||
) {}
|
||||
public get env(): BuildStepEnv {
|
||||
return this._env;
|
||||
}
|
||||
public staticContext(): any {
|
||||
return { ...this.staticContextContent };
|
||||
}
|
||||
public updateEnv(env: BuildStepEnv): void {
|
||||
this._env = env;
|
||||
}
|
||||
}
|
||||
|
||||
interface BuildContextParams {
|
||||
buildId?: string;
|
||||
logger?: bunyan;
|
||||
skipCleanup?: boolean;
|
||||
runtimePlatform?: BuildRuntimePlatform;
|
||||
projectSourceDirectory?: string;
|
||||
projectTargetDirectory?: string;
|
||||
relativeWorkingDirectory?: string;
|
||||
staticContextContent?: Record<string, any>;
|
||||
}
|
||||
|
||||
export function createStepContextMock({
|
||||
buildId,
|
||||
logger,
|
||||
skipCleanup,
|
||||
runtimePlatform,
|
||||
projectSourceDirectory,
|
||||
projectTargetDirectory,
|
||||
relativeWorkingDirectory,
|
||||
staticContextContent,
|
||||
}: BuildContextParams = {}): BuildStepContext {
|
||||
const globalCtx = createGlobalContextMock({
|
||||
buildId,
|
||||
logger,
|
||||
skipCleanup,
|
||||
runtimePlatform,
|
||||
projectSourceDirectory,
|
||||
projectTargetDirectory,
|
||||
relativeWorkingDirectory,
|
||||
staticContextContent,
|
||||
});
|
||||
return new BuildStepContext(globalCtx, {
|
||||
logger: logger ?? createMockLogger(),
|
||||
relativeWorkingDirectory,
|
||||
});
|
||||
}
|
||||
|
||||
export function createGlobalContextMock({
|
||||
logger,
|
||||
skipCleanup,
|
||||
runtimePlatform,
|
||||
projectSourceDirectory,
|
||||
projectTargetDirectory,
|
||||
relativeWorkingDirectory,
|
||||
staticContextContent,
|
||||
}: BuildContextParams = {}): BuildStepGlobalContext {
|
||||
const resolvedProjectTargetDirectory =
|
||||
projectTargetDirectory ?? path.join(os.tmpdir(), 'eas-build', uuidv4());
|
||||
return new BuildStepGlobalContext(
|
||||
new MockContextProvider(
|
||||
logger ?? createMockLogger(),
|
||||
runtimePlatform ?? BuildRuntimePlatform.LINUX,
|
||||
projectSourceDirectory ?? '/non/existent/dir',
|
||||
resolvedProjectTargetDirectory,
|
||||
relativeWorkingDirectory
|
||||
? path.resolve(resolvedProjectTargetDirectory, relativeWorkingDirectory)
|
||||
: resolvedProjectTargetDirectory,
|
||||
'/non/existent/dir',
|
||||
staticContextContent ?? {}
|
||||
),
|
||||
skipCleanup ?? false
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
import {
|
||||
Android,
|
||||
ArchiveSourceType,
|
||||
BuildMode,
|
||||
BuildTrigger,
|
||||
Ios,
|
||||
Platform,
|
||||
Workflow,
|
||||
} from '@expo/eas-build-job';
|
||||
import { randomUUID } from 'crypto';
|
||||
|
||||
const androidCredentials: Android.BuildSecrets['buildCredentials'] = {
|
||||
keystore: {
|
||||
dataBase64: 'MjEzNwo=',
|
||||
keystorePassword: 'pass1',
|
||||
keyAlias: 'alias',
|
||||
keyPassword: 'pass2',
|
||||
},
|
||||
};
|
||||
|
||||
const iosCredentials: Ios.BuildCredentials = {
|
||||
testapp: {
|
||||
provisioningProfileBase64: '',
|
||||
distributionCertificate: {
|
||||
dataBase64: '',
|
||||
password: '',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export function createTestAndroidJob({
|
||||
buildCredentials = androidCredentials,
|
||||
}: {
|
||||
buildCredentials?: Android.BuildSecrets['buildCredentials'];
|
||||
} = {}): Android.Job {
|
||||
return {
|
||||
mode: BuildMode.BUILD,
|
||||
platform: Platform.ANDROID,
|
||||
triggeredBy: BuildTrigger.EAS_CLI,
|
||||
type: Workflow.GENERIC,
|
||||
projectArchive: {
|
||||
type: ArchiveSourceType.URL,
|
||||
url: 'https://turtle-v2-test-fixtures.s3.us-east-2.amazonaws.com/project.tar.gz',
|
||||
},
|
||||
projectRootDirectory: '.',
|
||||
applicationArchivePath: './android/app/build/outputs/apk/release/*.apk',
|
||||
cache: {
|
||||
clear: false,
|
||||
disabled: false,
|
||||
paths: [],
|
||||
},
|
||||
secrets: {
|
||||
buildCredentials,
|
||||
},
|
||||
appId: randomUUID(),
|
||||
initiatingUserId: randomUUID(),
|
||||
};
|
||||
}
|
||||
|
||||
export function createTestIosJob({
|
||||
buildCredentials = iosCredentials,
|
||||
triggeredBy = BuildTrigger.EAS_CLI,
|
||||
workflowInterpolationContext,
|
||||
}: {
|
||||
buildCredentials?: Ios.BuildCredentials;
|
||||
triggeredBy?: Ios.Job['triggeredBy'];
|
||||
workflowInterpolationContext?: Ios.Job['workflowInterpolationContext'];
|
||||
} = {}): Ios.Job {
|
||||
return {
|
||||
mode: BuildMode.BUILD,
|
||||
platform: Platform.IOS,
|
||||
triggeredBy,
|
||||
type: Workflow.GENERIC,
|
||||
projectArchive: {
|
||||
type: ArchiveSourceType.URL,
|
||||
url: 'https://turtle-v2-test-fixtures.s3.us-east-2.amazonaws.com/project.tar.gz',
|
||||
},
|
||||
scheme: 'turtlebareproj',
|
||||
buildConfiguration: 'Release',
|
||||
applicationArchivePath: './ios/build/*.ipa',
|
||||
projectRootDirectory: '.',
|
||||
cache: {
|
||||
clear: false,
|
||||
disabled: false,
|
||||
paths: [],
|
||||
},
|
||||
secrets: {
|
||||
buildCredentials,
|
||||
},
|
||||
appId: randomUUID(),
|
||||
initiatingUserId: randomUUID(),
|
||||
workflowInterpolationContext,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { bunyan } from '@expo/logger';
|
||||
|
||||
export function createMockLogger({ logToConsole = false } = {}): bunyan {
|
||||
const logger = {
|
||||
info: jest.fn(logToConsole ? console.info : () => {}),
|
||||
debug: jest.fn(logToConsole ? console.debug : () => {}),
|
||||
error: jest.fn(logToConsole ? console.error : () => {}),
|
||||
warn: jest.fn(logToConsole ? console.warn : () => {}),
|
||||
child: jest.fn().mockImplementation(() => createMockLogger({ logToConsole })),
|
||||
} as unknown as bunyan;
|
||||
return logger;
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
import { AndroidConfig } from '@expo/config-plugins';
|
||||
import { vol } from 'memfs';
|
||||
import path from 'path';
|
||||
|
||||
import {
|
||||
AndroidMetadataName,
|
||||
androidGetNativelyDefinedChannelAsync,
|
||||
androidGetNativelyDefinedRuntimeVersionAsync,
|
||||
androidSetChannelNativelyAsync,
|
||||
androidSetRuntimeVersionNativelyAsync,
|
||||
} from '../expoUpdates';
|
||||
|
||||
jest.mock('fs');
|
||||
const originalFs = jest.requireActual('fs');
|
||||
|
||||
const channel = 'easupdatechannel';
|
||||
const manifestPath = '/app/android/app/src/main/AndroidManifest.xml';
|
||||
|
||||
afterEach(() => {
|
||||
vol.reset();
|
||||
});
|
||||
|
||||
describe(androidSetChannelNativelyAsync, () => {
|
||||
it('sets the channel', async () => {
|
||||
vol.fromJSON(
|
||||
{
|
||||
'android/app/src/main/AndroidManifest.xml': originalFs.readFileSync(
|
||||
path.join(__dirname, 'fixtures/NoMetadataAndroidManifest.xml'),
|
||||
'utf-8'
|
||||
),
|
||||
},
|
||||
'/app'
|
||||
);
|
||||
const ctx = {
|
||||
getReactNativeProjectDirectory: () => '/app',
|
||||
job: { updates: { channel } },
|
||||
logger: { info: () => {} },
|
||||
};
|
||||
|
||||
await androidSetChannelNativelyAsync(ctx as any);
|
||||
|
||||
const newAndroidManifest = await AndroidConfig.Manifest.readAndroidManifestAsync(manifestPath);
|
||||
const newValue = AndroidConfig.Manifest.getMainApplicationMetaDataValue(
|
||||
newAndroidManifest,
|
||||
AndroidMetadataName.UPDATES_CONFIGURATION_REQUEST_HEADERS_KEY
|
||||
);
|
||||
expect(newValue).toBeDefined();
|
||||
expect(JSON.parse(newValue!)).toEqual({ 'expo-channel-name': channel });
|
||||
});
|
||||
});
|
||||
|
||||
describe(androidGetNativelyDefinedChannelAsync, () => {
|
||||
it('gets the channel', async () => {
|
||||
vol.fromJSON(
|
||||
{
|
||||
'android/app/src/main/AndroidManifest.xml': originalFs.readFileSync(
|
||||
path.join(__dirname, 'fixtures/AndroidManifestWithChannel.xml'),
|
||||
'utf-8'
|
||||
),
|
||||
},
|
||||
'/app'
|
||||
);
|
||||
const ctx = {
|
||||
getReactNativeProjectDirectory: () => '/app',
|
||||
logger: { info: () => {} },
|
||||
};
|
||||
|
||||
await expect(androidGetNativelyDefinedChannelAsync(ctx as any)).resolves.toBe('staging-123');
|
||||
});
|
||||
});
|
||||
|
||||
describe(androidGetNativelyDefinedRuntimeVersionAsync, () => {
|
||||
it('gets the native runtime version', async () => {
|
||||
vol.fromJSON(
|
||||
{
|
||||
'android/app/src/main/AndroidManifest.xml': originalFs.readFileSync(
|
||||
path.join(__dirname, 'fixtures/AndroidManifestWithRuntimeVersion.xml'),
|
||||
'utf-8'
|
||||
),
|
||||
},
|
||||
'/app'
|
||||
);
|
||||
const ctx = {
|
||||
getReactNativeProjectDirectory: () => '/app',
|
||||
job: {},
|
||||
logger: { info: () => {} },
|
||||
};
|
||||
|
||||
const nativelyDefinedRuntimeVersion = await androidGetNativelyDefinedRuntimeVersionAsync(
|
||||
ctx as any
|
||||
);
|
||||
expect(nativelyDefinedRuntimeVersion).toBe('exampleruntimeversion');
|
||||
});
|
||||
});
|
||||
|
||||
describe(androidSetRuntimeVersionNativelyAsync, () => {
|
||||
it('sets the runtime version when nothing is set natively', async () => {
|
||||
vol.fromJSON(
|
||||
{
|
||||
'android/app/src/main/AndroidManifest.xml': originalFs.readFileSync(
|
||||
path.join(__dirname, 'fixtures/NoMetadataAndroidManifest.xml'),
|
||||
'utf-8'
|
||||
),
|
||||
},
|
||||
'/app'
|
||||
);
|
||||
const ctx = {
|
||||
getReactNativeProjectDirectory: () => '/app',
|
||||
job: {},
|
||||
logger: { info: () => {} },
|
||||
};
|
||||
|
||||
await androidSetRuntimeVersionNativelyAsync(ctx as any, '1.2.3');
|
||||
|
||||
const newAndroidManifest = await AndroidConfig.Manifest.readAndroidManifestAsync(manifestPath);
|
||||
const newValue = AndroidConfig.Manifest.getMainApplicationMetaDataValue(
|
||||
newAndroidManifest,
|
||||
AndroidMetadataName.RUNTIME_VERSION
|
||||
);
|
||||
expect(newValue).toBe('1.2.3');
|
||||
});
|
||||
it('updates the runtime version when value is already set natively', async () => {
|
||||
vol.fromJSON(
|
||||
{
|
||||
'android/app/src/main/AndroidManifest.xml': originalFs.readFileSync(
|
||||
path.join(__dirname, 'fixtures/AndroidManifestWithRuntimeVersion.xml'),
|
||||
'utf-8'
|
||||
),
|
||||
},
|
||||
'/app'
|
||||
);
|
||||
const ctx = {
|
||||
getReactNativeProjectDirectory: () => '/app',
|
||||
job: {},
|
||||
logger: { info: () => {} },
|
||||
};
|
||||
|
||||
await androidSetRuntimeVersionNativelyAsync(ctx as any, '1.2.3');
|
||||
|
||||
const newAndroidManifest = await AndroidConfig.Manifest.readAndroidManifestAsync(manifestPath);
|
||||
const newValue = AndroidConfig.Manifest.getMainApplicationMetaDataValue(
|
||||
newAndroidManifest,
|
||||
AndroidMetadataName.RUNTIME_VERSION
|
||||
);
|
||||
expect(newValue).toBe('1.2.3');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,14 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.expo.mycoolapp">
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<application android:name=".MainApplication" android:label="@string/app_name" android:icon="@mipmap/ic_launcher" android:roundIcon="@mipmap/ic_launcher_round" android:allowBackup="true" android:theme="@style/AppTheme">
|
||||
<activity android:name=".MainActivity" android:launchMode="singleTask" android:label="@string/app_name" android:configChanges="keyboard|keyboardHidden|orientation|screenSize" android:windowSoftInputMode="adjustResize">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity android:name="com.facebook.react.devsupport.DevSettingsActivity"/>
|
||||
<meta-data android:name="expo.modules.updates.EXPO_RELEASE_CHANNEL" android:value="default"/>
|
||||
<meta-data android:name="expo.modules.updates.UPDATES_CONFIGURATION_REQUEST_HEADERS_KEY" android:value="{"expo-channel-name":"staging-123"}"/>
|
||||
</application>
|
||||
</manifest>
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.expo.mycoolapp">
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<application android:name=".MainApplication" android:label="@string/app_name" android:icon="@mipmap/ic_launcher" android:roundIcon="@mipmap/ic_launcher_round" android:allowBackup="true" android:theme="@style/AppTheme">
|
||||
<activity android:name=".MainActivity" android:launchMode="singleTask" android:label="@string/app_name" android:configChanges="keyboard|keyboardHidden|orientation|screenSize" android:windowSoftInputMode="adjustResize">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity android:name="com.facebook.react.devsupport.DevSettingsActivity"/>
|
||||
<meta-data android:name="expo.modules.updates.EXPO_RELEASE_CHANNEL" android:value="example_channel"/>
|
||||
</application>
|
||||
</manifest>
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.expo.mycoolapp">
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<application android:name=".MainApplication" android:label="@string/app_name" android:icon="@mipmap/ic_launcher" android:roundIcon="@mipmap/ic_launcher_round" android:allowBackup="true" android:theme="@style/AppTheme">
|
||||
<activity android:name=".MainActivity" android:launchMode="singleTask" android:label="@string/app_name" android:configChanges="keyboard|keyboardHidden|orientation|screenSize" android:windowSoftInputMode="adjustResize">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity android:name="com.facebook.react.devsupport.DevSettingsActivity"/>
|
||||
<meta-data android:name="expo.modules.updates.EXPO_RELEASE_CHANNEL" android:value="default"/>
|
||||
<meta-data android:name="expo.modules.updates.EXPO_RUNTIME_VERSION" android:value="exampleruntimeversion"/>
|
||||
</application>
|
||||
</manifest>
|
||||
@@ -0,0 +1,27 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.expo.mycoolapp">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
|
||||
<application
|
||||
android:name=".MainApplication"
|
||||
android:label="@string/app_name"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:allowBackup="true"
|
||||
android:theme="@style/AppTheme">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:launchMode="singleTask"
|
||||
android:label="@string/app_name"
|
||||
android:configChanges="keyboard|keyboardHidden|orientation|screenSize"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity android:name="com.facebook.react.devsupport.DevSettingsActivity" />
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
@@ -0,0 +1,40 @@
|
||||
import { Android } from '@expo/eas-build-job';
|
||||
import fs from 'fs-extra';
|
||||
import nullthrows from 'nullthrows';
|
||||
import path from 'path';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { BuildContext } from '../context';
|
||||
|
||||
async function restoreCredentials(ctx: BuildContext<Android.Job>): Promise<void> {
|
||||
const { buildCredentials } = nullthrows(
|
||||
ctx.job.secrets,
|
||||
'Secrets must be defined for non-custom builds'
|
||||
);
|
||||
if (!buildCredentials) {
|
||||
// TODO: sentry (should be detected earlier)
|
||||
throw new Error('secrets are missing in the job object');
|
||||
}
|
||||
ctx.logger.info("Writing secrets to the project's directory");
|
||||
const keystorePath = path.join(ctx.buildDirectory, `keystore-${uuidv4()}`);
|
||||
await fs.writeFile(
|
||||
keystorePath,
|
||||
new Uint8Array(Buffer.from(buildCredentials.keystore.dataBase64, 'base64'))
|
||||
);
|
||||
const credentialsJson = {
|
||||
android: {
|
||||
keystore: {
|
||||
keystorePath,
|
||||
keystorePassword: buildCredentials.keystore.keystorePassword,
|
||||
keyAlias: buildCredentials.keystore.keyAlias,
|
||||
keyPassword: buildCredentials.keystore.keyPassword,
|
||||
},
|
||||
},
|
||||
};
|
||||
await fs.writeFile(
|
||||
path.join(ctx.buildDirectory, 'credentials.json'),
|
||||
JSON.stringify(credentialsJson)
|
||||
);
|
||||
}
|
||||
|
||||
export { restoreCredentials };
|
||||
@@ -0,0 +1,106 @@
|
||||
import { AndroidConfig } from '@expo/config-plugins';
|
||||
import { BuildJob, Job } from '@expo/eas-build-job';
|
||||
import assert from 'assert';
|
||||
import fs from 'fs-extra';
|
||||
|
||||
import { BuildContext } from '../context';
|
||||
|
||||
export enum AndroidMetadataName {
|
||||
UPDATES_CONFIGURATION_REQUEST_HEADERS_KEY = 'expo.modules.updates.UPDATES_CONFIGURATION_REQUEST_HEADERS_KEY',
|
||||
RUNTIME_VERSION = 'expo.modules.updates.EXPO_RUNTIME_VERSION',
|
||||
}
|
||||
|
||||
export async function androidSetRuntimeVersionNativelyAsync(
|
||||
ctx: BuildContext<Job>,
|
||||
runtimeVersion: string
|
||||
): Promise<void> {
|
||||
const manifestPath = await AndroidConfig.Paths.getAndroidManifestAsync(
|
||||
ctx.getReactNativeProjectDirectory()
|
||||
);
|
||||
|
||||
if (!(await fs.pathExists(manifestPath))) {
|
||||
throw new Error(`Couldn't find Android manifest at ${manifestPath}`);
|
||||
}
|
||||
|
||||
const androidManifest = await AndroidConfig.Manifest.readAndroidManifestAsync(manifestPath);
|
||||
const mainApp = AndroidConfig.Manifest.getMainApplicationOrThrow(androidManifest);
|
||||
AndroidConfig.Manifest.addMetaDataItemToMainApplication(
|
||||
mainApp,
|
||||
AndroidMetadataName.RUNTIME_VERSION,
|
||||
runtimeVersion,
|
||||
'value'
|
||||
);
|
||||
await AndroidConfig.Manifest.writeAndroidManifestAsync(manifestPath, androidManifest);
|
||||
}
|
||||
|
||||
export async function androidSetChannelNativelyAsync(ctx: BuildContext<BuildJob>): Promise<void> {
|
||||
assert(ctx.job.updates?.channel, 'updates.channel must be defined');
|
||||
|
||||
const manifestPath = await AndroidConfig.Paths.getAndroidManifestAsync(
|
||||
ctx.getReactNativeProjectDirectory()
|
||||
);
|
||||
|
||||
if (!(await fs.pathExists(manifestPath))) {
|
||||
throw new Error(`Couldn't find Android manifest at ${manifestPath}`);
|
||||
}
|
||||
|
||||
const androidManifest = await AndroidConfig.Manifest.readAndroidManifestAsync(manifestPath);
|
||||
const mainApp = AndroidConfig.Manifest.getMainApplicationOrThrow(androidManifest);
|
||||
const stringifiedUpdatesRequestHeaders = AndroidConfig.Manifest.getMainApplicationMetaDataValue(
|
||||
androidManifest,
|
||||
AndroidMetadataName.UPDATES_CONFIGURATION_REQUEST_HEADERS_KEY
|
||||
);
|
||||
AndroidConfig.Manifest.addMetaDataItemToMainApplication(
|
||||
mainApp,
|
||||
AndroidMetadataName.UPDATES_CONFIGURATION_REQUEST_HEADERS_KEY,
|
||||
JSON.stringify({
|
||||
...JSON.parse(stringifiedUpdatesRequestHeaders ?? '{}'),
|
||||
'expo-channel-name': ctx.job.updates.channel,
|
||||
}),
|
||||
'value'
|
||||
);
|
||||
await AndroidConfig.Manifest.writeAndroidManifestAsync(manifestPath, androidManifest);
|
||||
}
|
||||
|
||||
export async function androidGetNativelyDefinedChannelAsync(
|
||||
ctx: BuildContext<Job>
|
||||
): Promise<string | null> {
|
||||
const manifestPath = await AndroidConfig.Paths.getAndroidManifestAsync(
|
||||
ctx.getReactNativeProjectDirectory()
|
||||
);
|
||||
|
||||
if (!(await fs.pathExists(manifestPath))) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const androidManifest = await AndroidConfig.Manifest.readAndroidManifestAsync(manifestPath);
|
||||
const stringifiedUpdatesRequestHeaders = AndroidConfig.Manifest.getMainApplicationMetaDataValue(
|
||||
androidManifest,
|
||||
AndroidMetadataName.UPDATES_CONFIGURATION_REQUEST_HEADERS_KEY
|
||||
);
|
||||
try {
|
||||
const updatesRequestHeaders = JSON.parse(stringifiedUpdatesRequestHeaders ?? '{}');
|
||||
return updatesRequestHeaders['expo-channel-name'] ?? null;
|
||||
} catch (err: any) {
|
||||
throw new Error(
|
||||
`Failed to parse ${AndroidMetadataName.UPDATES_CONFIGURATION_REQUEST_HEADERS_KEY} from AndroidManifest.xml: ${err.message}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function androidGetNativelyDefinedRuntimeVersionAsync(
|
||||
ctx: BuildContext<Job>
|
||||
): Promise<string | null> {
|
||||
const manifestPath = await AndroidConfig.Paths.getAndroidManifestAsync(
|
||||
ctx.getReactNativeProjectDirectory()
|
||||
);
|
||||
if (!(await fs.pathExists(manifestPath))) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const androidManifest = await AndroidConfig.Manifest.readAndroidManifestAsync(manifestPath);
|
||||
return AndroidConfig.Manifest.getMainApplicationMetaDataValue(
|
||||
androidManifest,
|
||||
AndroidMetadataName.RUNTIME_VERSION
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
import { Android, Env, Job, Platform } from '@expo/eas-build-job';
|
||||
import { bunyan } from '@expo/logger';
|
||||
import spawn, { SpawnPromise, SpawnResult } from '@expo/turtle-spawn';
|
||||
import assert from 'assert';
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
|
||||
import { BuildContext } from '../context';
|
||||
import { getParentAndDescendantProcessPidsAsync } from '../utils/processes';
|
||||
|
||||
export async function ensureLFLineEndingsInGradlewScript<TJob extends Job>(
|
||||
ctx: BuildContext<TJob>
|
||||
): Promise<void> {
|
||||
const gradlewPath = path.join(ctx.getReactNativeProjectDirectory(), 'android', 'gradlew');
|
||||
const gradlewContent = await fs.readFile(gradlewPath, 'utf8');
|
||||
if (gradlewContent.includes('\r')) {
|
||||
ctx.logger.info('Replacing CRLF line endings with LF in gradlew script');
|
||||
await fs.writeFile(gradlewPath, gradlewContent.replace(/\r\n/g, '\n'), 'utf8');
|
||||
}
|
||||
}
|
||||
|
||||
export async function runGradleCommand(
|
||||
ctx: BuildContext<Job>,
|
||||
{
|
||||
logger,
|
||||
gradleCommand,
|
||||
androidDir,
|
||||
extraEnv,
|
||||
}: { logger: bunyan; gradleCommand: string; androidDir: string; extraEnv?: Env }
|
||||
): Promise<void> {
|
||||
logger.info(`Running 'gradlew ${gradleCommand}' in ${androidDir}`);
|
||||
await fs.chmod(path.join(androidDir, 'gradlew'), 0o755);
|
||||
const verboseFlag = ctx.env['EAS_VERBOSE'] === '1' ? '--info' : '';
|
||||
|
||||
const spawnPromise = spawn('bash', ['-c', `./gradlew ${gradleCommand} ${verboseFlag}`], {
|
||||
cwd: androidDir,
|
||||
logger,
|
||||
lineTransformer: (line?: string) => {
|
||||
if (!line || /^\.+$/.exec(line)) {
|
||||
return null;
|
||||
} else {
|
||||
return line;
|
||||
}
|
||||
},
|
||||
env: { ...ctx.env, ...extraEnv, ...resolveVersionOverridesEnvs(ctx) },
|
||||
});
|
||||
if (ctx.env.EAS_BUILD_RUNNER === 'eas-build' && process.platform === 'linux') {
|
||||
adjustOOMScore(spawnPromise, logger);
|
||||
}
|
||||
|
||||
await spawnPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* OOM Killer sometimes kills worker server while build is exceeding memory limits.
|
||||
* `oom_score_adj` is a value between -1000 and 1000 and it defaults to 0.
|
||||
* It defines which process is more likely to get killed (higher value more likely).
|
||||
*
|
||||
* This function sets oom_score_adj for Gradle process and all its child processes.
|
||||
*/
|
||||
function adjustOOMScore(spawnPromise: SpawnPromise<SpawnResult>, logger: bunyan): void {
|
||||
setTimeout(
|
||||
async () => {
|
||||
try {
|
||||
assert(spawnPromise.child.pid);
|
||||
const pids = await getParentAndDescendantProcessPidsAsync(spawnPromise.child.pid);
|
||||
await Promise.all(
|
||||
pids.map(async (pid: number) => {
|
||||
// Value 800 is just a guess here. It's probably higher than most other
|
||||
// process. I didn't want to set it any higher, because I'm not sure if OOM Killer
|
||||
// can start killing processes when there is still enough memory left.
|
||||
const oomScoreOverride = 800;
|
||||
await fs.writeFile(`/proc/${pid}/oom_score_adj`, `${oomScoreOverride}\n`);
|
||||
})
|
||||
);
|
||||
} catch (err: any) {
|
||||
logger.debug({ err, stderr: err?.stderr }, 'Failed to override oom_score_adj');
|
||||
}
|
||||
},
|
||||
// Wait 20 seconds to make sure all child processes are started
|
||||
20000
|
||||
);
|
||||
}
|
||||
|
||||
// Version envs should be set at the beginning of the build, but when building
|
||||
// from github those values are resolved later.
|
||||
function resolveVersionOverridesEnvs(ctx: BuildContext<Job>): Env {
|
||||
const extraEnvs: Env = {};
|
||||
if (
|
||||
ctx.job.platform === Platform.ANDROID &&
|
||||
ctx.job.version?.versionCode &&
|
||||
!ctx.env.EAS_BUILD_ANDROID_VERSION_CODE
|
||||
) {
|
||||
extraEnvs.EAS_BUILD_ANDROID_VERSION_CODE = ctx.job.version.versionCode;
|
||||
}
|
||||
if (
|
||||
ctx.job.platform === Platform.ANDROID &&
|
||||
ctx.job.version?.versionName &&
|
||||
!ctx.env.EAS_BUILD_ANDROID_VERSION_NAME
|
||||
) {
|
||||
extraEnvs.EAS_BUILD_ANDROID_VERSION_NAME = ctx.job.version.versionName;
|
||||
}
|
||||
return extraEnvs;
|
||||
}
|
||||
|
||||
export function resolveGradleCommand(job: Android.Job): string {
|
||||
if (job.gradleCommand) {
|
||||
return job.gradleCommand;
|
||||
} else if (job.developmentClient) {
|
||||
return ':app:assembleDebug';
|
||||
} else if (!job.buildType) {
|
||||
return ':app:bundleRelease';
|
||||
} else if (job.buildType === Android.BuildType.APK) {
|
||||
return ':app:assembleRelease';
|
||||
} else {
|
||||
return ':app:bundleRelease';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import { AndroidConfig } from '@expo/config-plugins';
|
||||
import { Android } from '@expo/eas-build-job';
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
|
||||
import { BuildContext } from '../context';
|
||||
import { EasBuildGradle } from '../templates/EasBuildGradle';
|
||||
const APPLY_EAS_BUILD_GRADLE_LINE = 'apply from: "./eas-build.gradle"';
|
||||
|
||||
export async function configureBuildGradle(ctx: BuildContext<Android.Job>): Promise<void> {
|
||||
ctx.logger.info('Injecting signing config into build.gradle');
|
||||
if (await fs.pathExists(getEasBuildGradlePath(ctx.getReactNativeProjectDirectory()))) {
|
||||
ctx.markBuildPhaseHasWarnings();
|
||||
ctx.logger.warn('eas-build.gradle script is deprecated, please remove it from your project.');
|
||||
}
|
||||
await deleteEasBuildGradle(ctx.getReactNativeProjectDirectory());
|
||||
await createEasBuildGradle(ctx.getReactNativeProjectDirectory());
|
||||
await addApplyToBuildGradle(ctx.getReactNativeProjectDirectory());
|
||||
}
|
||||
|
||||
async function deleteEasBuildGradle(projectRoot: string): Promise<void> {
|
||||
const easBuildGradlePath = getEasBuildGradlePath(projectRoot);
|
||||
await fs.remove(easBuildGradlePath);
|
||||
}
|
||||
|
||||
function getEasBuildGradlePath(projectRoot: string): string {
|
||||
return path.join(projectRoot, 'android/app/eas-build.gradle');
|
||||
}
|
||||
|
||||
async function createEasBuildGradle(projectRoot: string): Promise<void> {
|
||||
const easBuildGradlePath = getEasBuildGradlePath(projectRoot);
|
||||
await fs.writeFile(easBuildGradlePath, EasBuildGradle);
|
||||
}
|
||||
|
||||
async function addApplyToBuildGradle(projectRoot: string): Promise<void> {
|
||||
const buildGradlePath = AndroidConfig.Paths.getAppBuildGradleFilePath(projectRoot);
|
||||
const buildGradleContents = await fs.readFile(path.join(buildGradlePath), 'utf8');
|
||||
|
||||
if (hasLine(buildGradleContents, APPLY_EAS_BUILD_GRADLE_LINE)) {
|
||||
return;
|
||||
}
|
||||
|
||||
await fs.writeFile(
|
||||
buildGradlePath,
|
||||
`${buildGradleContents.trim()}\n${APPLY_EAS_BUILD_GRADLE_LINE}\n`
|
||||
);
|
||||
}
|
||||
|
||||
function hasLine(haystack: string, needle: string): boolean {
|
||||
return (
|
||||
haystack
|
||||
.replace(/\r\n/g, '\n')
|
||||
.split('\n')
|
||||
// Check for both single and double quotes
|
||||
.some(line => line === needle || line === needle.replace(/"/g, "'"))
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,293 @@
|
||||
import { BuildMode, BuildPhase, Job, Platform, errors } from '@expo/eas-build-job';
|
||||
import { vol } from 'memfs';
|
||||
import path from 'path';
|
||||
|
||||
import { resolveBuildPhaseErrorAsync } from '../detectError';
|
||||
|
||||
jest.mock('fs');
|
||||
const originalFs = jest.requireActual('fs');
|
||||
|
||||
afterEach(() => {
|
||||
vol.reset();
|
||||
});
|
||||
|
||||
describe(resolveBuildPhaseErrorAsync, () => {
|
||||
it('detects log for corrupted npm package', async () => {
|
||||
const fakeError = new Error();
|
||||
const err = await resolveBuildPhaseErrorAsync(
|
||||
fakeError,
|
||||
[
|
||||
'[stderr] WARN tarball tarball data for @typescript-eslint/[email protected] (sha512-cozo/GbwixVR0sgfHItz3t1yXu521yn71Wj6PlYCFA3WPhy51CUPkifFKfBis91bDclGmAY45hhaAXVjdn4new==) seems to be corrupted. Trying again.',
|
||||
],
|
||||
{
|
||||
job: { platform: Platform.ANDROID } as Job,
|
||||
phase: BuildPhase.INSTALL_DEPENDENCIES,
|
||||
env: {},
|
||||
},
|
||||
'/fake/path'
|
||||
);
|
||||
expect(err.errorCode).toBe('NPM_CORRUPTED_PACKAGE');
|
||||
expect(err.userFacingErrorCode).toBe(errors.ErrorCode.UNKNOWN_ERROR);
|
||||
});
|
||||
|
||||
it('detects log for invalid bundler and reports it to user', async () => {
|
||||
const fakeError = new Error();
|
||||
const err = await resolveBuildPhaseErrorAsync(
|
||||
fakeError,
|
||||
[
|
||||
"/System/Library/Frameworks/Ruby.framework/Versions/2.6/usr/lib/ruby/2.6.0/rubygems/dependency.rb:313:in `to_specs': Could not find 'bundler' (2.2.3) required by your /Users/expo/project/build/ios/Gemfile.lock. (Gem::MissingSpecVersionError)",
|
||||
],
|
||||
{
|
||||
job: { platform: Platform.IOS } as Job,
|
||||
phase: BuildPhase.RUN_FASTLANE,
|
||||
env: {},
|
||||
},
|
||||
'/fake/path'
|
||||
);
|
||||
expect(err.errorCode).toBe('EAS_BUILD_UNSUPPORTED_BUNDLER_VERSION_ERROR');
|
||||
expect(err.userFacingErrorCode).toBe('EAS_BUILD_UNSUPPORTED_BUNDLER_VERSION_ERROR');
|
||||
});
|
||||
|
||||
it('does not detect errors if they show up in different build phase', async () => {
|
||||
const fakeError = new Error();
|
||||
const err = await resolveBuildPhaseErrorAsync(
|
||||
fakeError,
|
||||
[
|
||||
"/System/Library/Frameworks/Ruby.framework/Versions/2.6/usr/lib/ruby/2.6.0/rubygems/dependency.rb:313:in `to_specs': Could not find 'bundler' (2.2.3) required by your /Users/expo/project/build/ios/Gemfile.lock. (Gem::MissingSpecVersionError)",
|
||||
],
|
||||
{
|
||||
job: { platform: Platform.IOS } as Job,
|
||||
phase: BuildPhase.INSTALL_DEPENDENCIES, // it should be in RUN_FASTLANE
|
||||
env: {},
|
||||
},
|
||||
'/fake/path'
|
||||
);
|
||||
expect(err.errorCode).toBe(errors.ErrorCode.UNKNOWN_ERROR);
|
||||
expect(err.userFacingErrorCode).toBe(errors.ErrorCode.UNKNOWN_ERROR);
|
||||
});
|
||||
|
||||
it('detects npm cache error if cache is enabled', async () => {
|
||||
const mockEnv = {
|
||||
EAS_BUILD_NPM_CACHE_URL: 'https://dominik.sokal.pl/npm/cache',
|
||||
};
|
||||
|
||||
const fakeError = new Error();
|
||||
const err = await resolveBuildPhaseErrorAsync(
|
||||
fakeError,
|
||||
[`Blah blah Error ... ${mockEnv.EAS_BUILD_NPM_CACHE_URL}`],
|
||||
{
|
||||
job: { platform: Platform.ANDROID } as Job,
|
||||
phase: BuildPhase.INSTALL_DEPENDENCIES,
|
||||
env: mockEnv,
|
||||
},
|
||||
'/fake/path'
|
||||
);
|
||||
expect(err.errorCode).toBe('NPM_CACHE_ERROR');
|
||||
expect(err.userFacingErrorCode).toBe(errors.ErrorCode.UNKNOWN_ERROR);
|
||||
});
|
||||
|
||||
it('does not detect npm cache error if cache is disabled', async () => {
|
||||
const mockEnv = {
|
||||
EAS_BUILD_NPM_CACHE_URL: 'https://dominik.sokal.pl/npm/cache',
|
||||
};
|
||||
|
||||
const fakeError = new Error();
|
||||
const err = await resolveBuildPhaseErrorAsync(
|
||||
fakeError,
|
||||
[`Blah blah Error ... ${mockEnv.EAS_BUILD_NPM_CACHE_URL}`],
|
||||
{
|
||||
job: { platform: Platform.ANDROID } as Job,
|
||||
phase: BuildPhase.INSTALL_DEPENDENCIES,
|
||||
env: {},
|
||||
},
|
||||
'/fake/path'
|
||||
);
|
||||
expect(err.errorCode).toBe(errors.ErrorCode.UNKNOWN_ERROR);
|
||||
expect(err.userFacingErrorCode).toBe(errors.ErrorCode.UNKNOWN_ERROR);
|
||||
});
|
||||
|
||||
it('detects xcode line error', async () => {
|
||||
vol.fromJSON({
|
||||
'/path/to/xcodelogs.log': originalFs.readFileSync(
|
||||
path.resolve('./src/buildErrors/__tests__/fixtures/xcode.log'),
|
||||
'utf-8'
|
||||
),
|
||||
});
|
||||
|
||||
const fakeError = new Error();
|
||||
const err = await resolveBuildPhaseErrorAsync(
|
||||
fakeError,
|
||||
[''],
|
||||
{
|
||||
job: { platform: Platform.IOS } as Job,
|
||||
phase: BuildPhase.RUN_FASTLANE,
|
||||
env: {},
|
||||
},
|
||||
'/path/to/'
|
||||
);
|
||||
expect(err.errorCode).toBe('XCODE_RESOURCE_BUNDLE_CODE_SIGNING_ERROR');
|
||||
expect(err.userFacingErrorCode).toBe('XCODE_RESOURCE_BUNDLE_CODE_SIGNING_ERROR');
|
||||
});
|
||||
|
||||
it('detects minimum deployment target error correctly', async () => {
|
||||
const fakeError = new Error();
|
||||
const err = await resolveBuildPhaseErrorAsync(
|
||||
fakeError,
|
||||
[
|
||||
'CocoaPods could not find compatible versions for pod "react-native-google-maps":16 In Podfile:17 react-native-google-maps (from `/Users/expo/workingdir/build/node_modules/react-native-maps`)18Specs satisfying the `react-native-google-maps (from `/Users/expo/workingdir/build/node_modules/react-native-maps`)` dependency were found, but they required a higher minimum deployment target.19Error: Compatible versions of some pods could not be resolved.',
|
||||
],
|
||||
{
|
||||
job: { platform: Platform.IOS } as Job,
|
||||
phase: BuildPhase.INSTALL_PODS,
|
||||
env: {},
|
||||
},
|
||||
'/fake/path'
|
||||
);
|
||||
expect(err.errorCode).toBe('EAS_BUILD_HIGHER_MINIMUM_DEPLOYMENT_TARGET_ERROR');
|
||||
expect(err.userFacingErrorCode).toBe('EAS_BUILD_HIGHER_MINIMUM_DEPLOYMENT_TARGET_ERROR');
|
||||
});
|
||||
|
||||
it('detects provisioning profile mismatch error correctly', async () => {
|
||||
const fakeError = new Error();
|
||||
const err = await resolveBuildPhaseErrorAsync(
|
||||
fakeError,
|
||||
[
|
||||
`No provisioning profile for application: '_floatsignTemp/Payload/EcoBatteryPREVIEW.app' with bundle identifier 'com.ecobattery.ecobattery-preview'`,
|
||||
],
|
||||
{
|
||||
job: { platform: Platform.IOS, mode: BuildMode.RESIGN } as Job,
|
||||
phase: BuildPhase.RUN_FASTLANE,
|
||||
env: {},
|
||||
},
|
||||
'/fake/path'
|
||||
);
|
||||
expect(err.errorCode).toBe('EAS_BUILD_RESIGN_PROVISIONING_PROFILE_MISMATCH_ERROR');
|
||||
expect(err.userFacingErrorCode).toBe('EAS_BUILD_RESIGN_PROVISIONING_PROFILE_MISMATCH_ERROR');
|
||||
});
|
||||
|
||||
it('detects generic "Run Fastlane" error for resign correctly', async () => {
|
||||
const fakeError = new Error();
|
||||
const err = await resolveBuildPhaseErrorAsync(
|
||||
fakeError,
|
||||
[`other error`],
|
||||
{
|
||||
job: { platform: Platform.IOS, mode: BuildMode.RESIGN } as Job,
|
||||
phase: BuildPhase.RUN_FASTLANE,
|
||||
env: {},
|
||||
},
|
||||
'/fake/path'
|
||||
);
|
||||
expect(err.errorCode).toBe('EAS_BUILD_UNKNOWN_FASTLANE_RESIGN_ERROR');
|
||||
expect(err.userFacingErrorCode).toBe('EAS_BUILD_UNKNOWN_FASTLANE_RESIGN_ERROR');
|
||||
});
|
||||
|
||||
it('detects build error in "Run Fastlane" phase correctly', async () => {
|
||||
const fakeError = new Error();
|
||||
const err = await resolveBuildPhaseErrorAsync(
|
||||
fakeError,
|
||||
[`some build error`],
|
||||
{
|
||||
job: { platform: Platform.IOS, mode: BuildMode.BUILD } as Job,
|
||||
phase: BuildPhase.RUN_FASTLANE,
|
||||
env: {},
|
||||
},
|
||||
'/fake/path'
|
||||
);
|
||||
expect(err.errorCode).toBe('EAS_BUILD_UNKNOWN_FASTLANE_ERROR');
|
||||
expect(err.userFacingErrorCode).toBe('EAS_BUILD_UNKNOWN_FASTLANE_ERROR');
|
||||
});
|
||||
|
||||
it('detects provisioning profile mismatch error correctly', async () => {
|
||||
const xcodeLogs = `Intermediates.noindex/Pods.build/Release-iphonesimulator/Yoga.build/Objects-normal/arm64/82b82416624d2658e5098eb0a28c15c5-common-args.resp
|
||||
-target arm64-apple-ios15.0-simulator '-std=gnu++14' '-stdlib=libc++' -fmodules '-fmodules-cache-path=/Users/expo/workingdir/build/packages/apps/NFLNetwork/ios/build/ModuleCache.noindex' '-fmodule-name=yoga' -fpascal-strings -Os -fno-common '-DPOD_CONFIGURATION_RELEASE=1' '-DCOCOAPODS=1' -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator18.1.sdk -g -iquote /Users/expo/workingdir/build/packages/apps/NFLNetwork/ios/build/Build/Intermediates.noindex/Pods.build/Release-iphonesimulator/Yoga.build/Yoga-generated-files.hmap -I/Users/expo/workingdir/build/packages/apps/NFLNetwork/ios/build/Build/Intermediates.noindex/Pods.build/Release-iphonesimulator/Yoga.build/Yoga-own-target-headers.hmap -I/Users/expo/workingdir/build/packages/apps/NFLNetwork/ios/build/Build/Intermediates.noindex/Pods.build/Release-iphonesimulator/Yoga.build/Yoga-all-non-framework-target-headers.hmap -ivfsoverlay /Users/expo/workingdir/build/packages/apps/NFLNetwork/ios/build/Build/Intermediates.noindex/Pods.build/Release-iphonesimulator/Pods-8699adb1dd336b26511df848a716bd42-VFS-iphonesimulator/all-product-headers.yaml -iquote /Users/expo/workingdir/build/packages/apps/NFLNetwork/ios/build/Build/Intermediates.noindex/Pods.build/Release-iphonesimulator/Yoga.build/Yoga-project-headers.hmap -I/Users/expo/workingdir/build/packages/apps/NFLNetwork/ios/build/Build/Products/Release-iphonesimulator/Yoga/include -I/Users/expo/workingdir/build/packages/apps/NFLNetwork/ios/Pods/Headers/Private -I/Users/expo/workingdir/build/packages/apps/NFLNetwork/ios/Pods/Headers/Private/Yoga -I/Users/expo/workingdir/build/packages/apps/NFLNetwork/ios/Pods/Headers/Public -I/Users/expo/workingdir/build/packages/apps/NFLNetwork/ios/Pods/Headers/Public/Yoga -I/Users/expo/workingdir/build/packages/apps/NFLNetwork/ios/build/Build/Intermediates.noindex/Pods.build/Release-iphonesimulator/Yoga.build/DerivedSources-normal/arm64 -I/Users/expo/workingdir/build/packages/apps/NFLNetwork/ios/build/Build/Intermediates.noindex/Pods.build/Release-iphonesimulator/Yoga.build/DerivedSources/arm64 -I/Users/expo/workingdir/build/packages/apps/NFLNetwork/ios/build/Build/Intermediates.noindex/Pods.build/Release-iphonesimulator/Yoga.build/DerivedSources -F/Users/expo/workingdir/build/packages/apps/NFLNetwork/ios/build/Build/Products/Release-iphonesimulator/Yoga
|
||||
MkDir /Users/expo/workingdir/build/packages/apps/NFLNetwork/ios/build/Build/Products/Release-iphonesimulator/expo-dev-menu/EXDevMenu.bundle (in target 'expo-dev-menu-EXDevMenu' from project 'Pods')
|
||||
cd /Users/expo/workingdir/build/packages/apps/NFLNetwork/ios/Pods
|
||||
/bin/mkdir -p /Users/expo/workingdir/build/packages/apps/NFLNetwork/ios/build/Build/Products/Release-iphonesimulator/expo-dev-menu/EXDevMenu.bundle
|
||||
|
||||
MkDir /Users/expo/workingdir/build/packages/apps/NFLNetwork/ios/build/Build/Products/Release-iphonesimulator/expo-dev-launcher/EXDevLauncher.bundle (in target 'expo-dev-launcher-EXDevLauncher' from project 'Pods')
|
||||
cd /Users/expo/workingdir/build/packages/apps/NFLNetwork/ios/Pods
|
||||
/bin/mkdir -p /Users/expo/workingdir/build/packages/apps/NFLNetwork/ios/build/Build/Products/Release-iphonesimulator/expo-dev-launcher/EXDevLauncher.bundle
|
||||
|
||||
ProcessXCFramework /Users/expo/workingdir/build/packages/video/ios/Vendor/dependency/ProgrammaticAccessLibrary.xcframework /Users/expo/workingdir/build/packages/apps/NFLNetwork/ios/build/Build/Products/Release-iphonesimulator/ProgrammaticAccessLibrary.framework ios simulator
|
||||
cd /Users/expo/workingdir/build/packages/apps/NFLNetwork/ios
|
||||
builtin-process-xcframework --xcframework /Users/expo/workingdir/build/packages/video/ios/Vendor/dependency/ProgrammaticAccessLibrary.xcframework --platform ios --environment simulator --target-path /Users/expo/workingdir/build/packages/apps/NFLNetwork/ios/build/Build/Products/Release-iphonesimulator
|
||||
/Users/expo/workingdir/build/packages/video/ios/Vendor/dependency/ProgrammaticAccessLibrary.xcframework:1:1: error: The signature of “ProgrammaticAccessLibrary.xcframework” cannot be verified.
|
||||
note: A sealed resource is missing or invalid
|
||||
note: /Users/expo/workingdir/build/packages/video/ios/Vendor/dependency/ProgrammaticAccessLibrary.xcframework: a sealed resource is missing or invalid
|
||||
file modified: /Users/expo/workingdir/build/packages/video/ios/Vendor/dependency/ProgrammaticAccessLibrary.xcframework/ios-arm64_x86_64-simulator/ProgrammaticAccessLibrary.framework/ProgrammaticAccessLibrary
|
||||
file modified: /Users/expo/workingdir/build/packages/video/ios/Vendor/dependency/ProgrammaticAccessLibrary.xcframework/ios-arm64/ProgrammaticAccessLibrary.framework/ProgrammaticAccessLibrary
|
||||
error: Some other error
|
||||
log
|
||||
log
|
||||
log
|
||||
error: The last one
|
||||
log
|
||||
note`;
|
||||
|
||||
vol.fromJSON({
|
||||
'/path/to/xcode.log': xcodeLogs,
|
||||
});
|
||||
|
||||
const fakeError = new Error();
|
||||
const err = await resolveBuildPhaseErrorAsync(
|
||||
fakeError,
|
||||
[`some logs`],
|
||||
{
|
||||
job: { platform: Platform.IOS, mode: BuildMode.BUILD } as Job,
|
||||
phase: BuildPhase.RUN_FASTLANE,
|
||||
env: {},
|
||||
},
|
||||
'/path/to'
|
||||
);
|
||||
expect(err.errorCode).toBe('XCODE_BUILD_ERROR');
|
||||
expect(err.userFacingErrorCode).toBe('XCODE_BUILD_ERROR');
|
||||
expect(err.userFacingMessage)
|
||||
.toBe(`The "Run fastlane" step failed because of an error in the Xcode build process. We automatically detected following errors in your Xcode build logs:
|
||||
- The signature of “ProgrammaticAccessLibrary.xcframework” cannot be verified.
|
||||
- Some other error
|
||||
- The last one
|
||||
Refer to "Xcode Logs" below for additional, more detailed logs.`);
|
||||
});
|
||||
|
||||
it('detects MAVEN_CACHE_ERROR correctly', async () => {
|
||||
const err = await resolveBuildPhaseErrorAsync(
|
||||
new Error(),
|
||||
[`https://szymon.pl/maven/cache`],
|
||||
{
|
||||
job: { platform: Platform.ANDROID, mode: BuildMode.BUILD } as Job,
|
||||
phase: BuildPhase.RUN_GRADLEW,
|
||||
env: {
|
||||
EAS_BUILD_MAVEN_CACHE_URL: 'https://szymon.pl/maven/cache',
|
||||
},
|
||||
},
|
||||
'/fake/path'
|
||||
);
|
||||
|
||||
expect(err.errorCode).toBe('MAVEN_CACHE_ERROR');
|
||||
expect(err.userFacingErrorCode).toBe('EAS_BUILD_UNKNOWN_GRADLE_ERROR');
|
||||
expect(err.userFacingMessage).toBe(
|
||||
`Gradle build failed with unknown error. See logs for the "Run gradlew" phase for more information.`
|
||||
);
|
||||
});
|
||||
|
||||
it('does not throw MAVEN_CACHE_ERROR if "Could not find BlurView-version-2.0.3.jar" log is present', async () => {
|
||||
const err = await resolveBuildPhaseErrorAsync(
|
||||
new Error(),
|
||||
[`Could not find BlurView-version-2.0.3.jar sth sthelse https://szymon.pl/maven/cache`],
|
||||
{
|
||||
job: { platform: Platform.ANDROID, mode: BuildMode.BUILD } as Job,
|
||||
phase: BuildPhase.RUN_GRADLEW,
|
||||
env: {
|
||||
EAS_BUILD_MAVEN_CACHE_URL: 'https://szymon.pl/maven/cache',
|
||||
},
|
||||
},
|
||||
'/fake/path'
|
||||
);
|
||||
|
||||
expect(err.errorCode).toBe('EAS_BUILD_UNKNOWN_GRADLE_ERROR');
|
||||
expect(err.userFacingErrorCode).toBe('EAS_BUILD_UNKNOWN_GRADLE_ERROR');
|
||||
expect(err.userFacingMessage).toBe(
|
||||
`Gradle build failed with unknown error. See logs for the "Run gradlew" phase for more information.`
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1 @@
|
||||
/Users/expo/workingdir/build/managed/ios/Pods/Pods.xcodeproj: error: Signing for "EXConstants-EXConstants" requires a development team. Select a development team in the Signing & Capabilities editor. (in target 'EXConstants-EXConstants' from project 'Pods')
|
||||
@@ -0,0 +1,361 @@
|
||||
import { BuildPhase, Platform } from '@expo/eas-build-job';
|
||||
import escapeRegExp from 'lodash/escapeRegExp';
|
||||
|
||||
import { ErrorContext, ErrorHandler } from './errors.types';
|
||||
|
||||
export class TrackedBuildError extends Error {
|
||||
constructor(
|
||||
public errorCode: string,
|
||||
public message: string
|
||||
) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
||||
export const buildErrorHandlers: ErrorHandler<TrackedBuildError>[] = [
|
||||
{
|
||||
platform: Platform.IOS,
|
||||
phase: BuildPhase.INSTALL_PODS,
|
||||
// example log:
|
||||
// CDN: trunk URL couldn't be downloaded: https://cdn.jsdelivr.net/cocoa/Specs/2/a/e/MultiplatformBleAdapter/0.0.3/MultiplatformBleAdapter.podspec.json Response: 429 429: Too Many Requests
|
||||
regexp: /CDN: trunk URL couldn't be downloaded.* Response: 429 429: Too Many Requests/,
|
||||
createError: () =>
|
||||
new TrackedBuildError('COCOAPODS_TO_MANY_REQUEST', 'cocoapods: too many requests'),
|
||||
},
|
||||
{
|
||||
phase: BuildPhase.INSTALL_DEPENDENCIES,
|
||||
// Host key verification failed.
|
||||
// fatal: Could not read from remote repository.
|
||||
regexp: /Host key verification failed\.\nfatal: Could not read from remote repository/,
|
||||
createError: () => new TrackedBuildError('NPM_INSTALL_SSH_AUTHENTICATION', 'Missing ssh key.'),
|
||||
},
|
||||
{
|
||||
phase: BuildPhase.INSTALL_DEPENDENCIES,
|
||||
// error [email protected]: The engine "node" is incompatible with this module. Expected version "14". Got "16.13.2"
|
||||
// error Found incompatible module.
|
||||
regexp: /The engine "node" is incompatible with this module\. Expected version/,
|
||||
createError: () =>
|
||||
new TrackedBuildError('NODE_ENGINE_INCOMPATIBLE', 'node: Incompatible engine field.'),
|
||||
},
|
||||
{
|
||||
phase: BuildPhase.INSTALL_DEPENDENCIES,
|
||||
// error An unexpected error occurred: "https://registry.yarnpkg.com/@react-native/normalize-color/-/normalize-color-2.0.0.tgz: Request failed \"500 Internal Server Error\"".
|
||||
// or
|
||||
// error An unexpected error occurred: "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz: Request failed \"503 Service Unavailable\"".
|
||||
regexp: /An unexpected error occurred: "https:\/\/registry.yarnpkg.com\/.*Request failed \\"5/,
|
||||
createError: () => new TrackedBuildError('YARN_REGISTRY_5XX_RESPONSE', 'yarn: 5xx response.'),
|
||||
},
|
||||
{
|
||||
phase: BuildPhase.PREBUILD,
|
||||
// Input is required, but Expo CLI is in non-interactive mode
|
||||
// or
|
||||
// CommandError: Input is required, but 'npx expo' is in non-interactive mode.
|
||||
regexp: /Input is required, but .* is in non-interactive mode/,
|
||||
createError: () =>
|
||||
new TrackedBuildError(
|
||||
'EXPO_CLI_INPUT_REQUIRED_ERROR',
|
||||
`expo-cli: Input required in non-interactive mode.`
|
||||
),
|
||||
},
|
||||
{
|
||||
platform: Platform.IOS,
|
||||
phase: BuildPhase.PREBUILD,
|
||||
// [03:03:05] [ios.infoPlist]: withIosInfoPlistBaseMod: GoogleService-Info.plist is empty
|
||||
regexp: /withIosInfoPlistBaseMod: GoogleService-Info\.plist is empty/,
|
||||
createError: () =>
|
||||
new TrackedBuildError(
|
||||
'EXPO_CLI_EMPTY_GOOGLE_SERVICES_PLIST_ERROR',
|
||||
`expo-cli: Empty GoogleService-Info.plist.`
|
||||
),
|
||||
},
|
||||
{
|
||||
platform: Platform.IOS,
|
||||
phase: BuildPhase.PREBUILD,
|
||||
// [01:52:04] [ios.xcodeproj]: withIosXcodeprojBaseMod: Path to GoogleService-Info.plist is not defined. Please specify the `expo.ios.googleServicesFile` field in app.json.
|
||||
regexp: /withIosXcodeprojBaseMod: Path to GoogleService-Info\.plist is not defined/,
|
||||
createError: () =>
|
||||
new TrackedBuildError(
|
||||
'EXPO_CLI_NOT_DEFINED_GOOGLE_SERVICES_PLIST_ERROR',
|
||||
`expo-cli: Path to GoogleService-Info.plist is not defined.`
|
||||
),
|
||||
},
|
||||
{
|
||||
phase: BuildPhase.PREBUILD,
|
||||
// Error: [android.dangerous]: withAndroidDangerousBaseMod: ENOENT: no such file or directory, open './assets/adaptive-icon.png'
|
||||
// at Object.openSync (node:fs:585:3)
|
||||
// at readFileSync (node:fs:453:35)
|
||||
// at calculateHash (/home/expo/workingdir/build/node_modules/@expo/image-utils/build/Cache.js:14:91)
|
||||
// at createCacheKey (/home/expo/workingdir/build/node_modules/@expo/image-utils/build/Cache.js:19:18)
|
||||
// at Object.createCacheKeyWithDirectoryAsync (/home/expo/workingdir/build/node_modules/@expo/image-utils/build/Cache.js:24:33)
|
||||
// at generateImageAsync (/home/expo/workingdir/build/node_modules/@expo/image-utils/build/Image.js:151:34)
|
||||
// at async generateIconAsync (/home/expo/workingdir/build/node_modules/@expo/prebuild-config/build/plugins/icons/withAndroidIcons.js:369:11)
|
||||
// at async /home/expo/workingdir/build/node_modules/@expo/prebuild-config/build/plugins/icons/withAndroidIcons.js:310:21
|
||||
// at async Promise.all (index 0)
|
||||
// at async generateMultiLayerImageAsync (/home/expo/workingdir/build/node_modules/@expo/prebuild-config/build/plugins/icons/withAndroidIcons.js:306:3)
|
||||
// or
|
||||
// Error: [ios.dangerous]: withIosDangerousBaseMod: ENOENT: no such file or directory, open './assets/images/app_icon_staging.png'
|
||||
// at Object.openSync (fs.js:497:3)
|
||||
// at readFileSync (fs.js:393:35)
|
||||
// at calculateHash (/Users/expo/workingdir/build/node_modules/@expo/image-utils/build/Cache.js:14:91)
|
||||
// at createCacheKey (/Users/expo/workingdir/build/node_modules/@expo/image-utils/build/Cache.js:19:18)
|
||||
// at Object.createCacheKeyWithDirectoryAsync (/Users/expo/workingdir/build/node_modules/@expo/image-utils/build/Cache.js:24:33)
|
||||
// at generateImageAsync (/Users/expo/workingdir/build/node_modules/@expo/image-utils/build/Image.js:151:34)
|
||||
// at async setIconsAsync (/Users/expo/workingdir/build/node_modules/@expo/prebuild-config/build/plugins/icons/withIosIcons.js:169:15)
|
||||
// at async /Users/expo/workingdir/build/node_modules/@expo/prebuild-config/build/plugins/icons/withIosIcons.js:71:5
|
||||
// at async action (/Users/expo/workingdir/build/node_modules/@expo/config-plugins/build/plugins/withMod.js:235:23)
|
||||
// at async interceptingMod (/Users/expo/workingdir/build/node_modules/@expo/config-plugins/build/plugins/withMod.js:126:21)
|
||||
regexp:
|
||||
/ENOENT: no such file or directory[\s\S]*prebuild-config\/build\/plugins\/icons\/with(Android|Ios)Icons\.js/,
|
||||
createError: () => new TrackedBuildError('EXPO_CLI_MISSING_ICON', 'expo-cli: Missing icon.'),
|
||||
},
|
||||
{
|
||||
phase: BuildPhase.PREBUILD,
|
||||
// Cannot determine which native SDK version your project uses because the module `expo` is not installed. Please install it with `yarn add expo` and try again.
|
||||
regexp:
|
||||
/Cannot determine which native SDK version your project uses because the module `expo` is not installed/,
|
||||
createError: () =>
|
||||
new TrackedBuildError('EXPO_CLI_EXPO_PACKAGE_MISSING', 'expo-cli: "expo" package missing.'),
|
||||
},
|
||||
{
|
||||
phase: BuildPhase.INSTALL_PODS,
|
||||
// The Swift pod `FirebaseCoreInternal` depends upon `GoogleUtilities`, which does not define modules. To opt into those targets generating module maps (which is necessary to import them from Swift when building as static
|
||||
regexp: /The Swift pod .* depends upon .* which does not define modules/,
|
||||
createError: () =>
|
||||
new TrackedBuildError(
|
||||
'SWIFT_POD_INCOMPATIBLE_DEPENDENCY',
|
||||
'pod: Swift pod depends on a pod that does not define modules.'
|
||||
),
|
||||
},
|
||||
{
|
||||
platform: Platform.IOS,
|
||||
phase: BuildPhase.INSTALL_PODS,
|
||||
// Adding spec repo `24-repository-cocoapods-proxy` with CDN `http://10.254.24.7:8081/repository/cocoapods-proxy/`
|
||||
// [!] No podspec exists at path `/Users/expo/.cocoapods/repos/24-repository-cocoapods-proxy/Specs/1/9/2/libwebp/1.2.0/libwebp.podspec.json`.
|
||||
regexp: /Adding spec repo .* with CDN .*\n\s*\[!\] No podspec exists at path `(.*)`/,
|
||||
// Some pods are hosted on git registries that are not supported e.g. chromium.googlesource.com
|
||||
createError: (match: RegExpMatchArray) =>
|
||||
new TrackedBuildError(
|
||||
'COCOAPODS_CACHE_INCOMPATIBLE_REPO_ERROR',
|
||||
`cocoapods: Missing podspec ${match[1]}.`
|
||||
),
|
||||
},
|
||||
{
|
||||
platform: Platform.IOS,
|
||||
phase: BuildPhase.INSTALL_PODS,
|
||||
// [!] Invalid `Podfile` file: 783: unexpected token at 'info Run CLI with --verbose flag for more details.
|
||||
regexp:
|
||||
/\[!\] Invalid `Podfile` file: .* unexpected token at 'info Run CLI with --verbose flag for more details./,
|
||||
createError: () =>
|
||||
new TrackedBuildError('NODE_ENV_PRODUCTION_DEFINED', 'npm: NODE_ENV=production was defined.'),
|
||||
},
|
||||
...[BuildPhase.INSTALL_DEPENDENCIES, BuildPhase.PREBUILD].map(phase => ({
|
||||
phase,
|
||||
// example log:
|
||||
// [stderr] WARN tarball tarball data for @typescript-eslint/[email protected] (sha512-cozo/GbwixVR0sgfHItz3t1yXu521yn71Wj6PlYCFA3WPhy51CUPkifFKfBis91bDclGmAY45hhaAXVjdn4new==) seems to be corrupted. Trying again.
|
||||
regexp: /tarball tarball data for ([^ ]*) .* seems to be corrupted. Trying again/,
|
||||
createError: (match: RegExpMatchArray) =>
|
||||
new TrackedBuildError('NPM_CORRUPTED_PACKAGE', `npm: corrupted package ${match[1]}`),
|
||||
})),
|
||||
...[BuildPhase.INSTALL_DEPENDENCIES, BuildPhase.PREBUILD].map(phase => ({
|
||||
phase,
|
||||
regexp: ({ env }: ErrorContext) =>
|
||||
env.EAS_BUILD_NPM_CACHE_URL
|
||||
? new RegExp(escapeRegExp(env.EAS_BUILD_NPM_CACHE_URL))
|
||||
: undefined,
|
||||
createError: () => new TrackedBuildError('NPM_CACHE_ERROR', `npm: cache error`),
|
||||
})),
|
||||
{
|
||||
platform: Platform.IOS,
|
||||
phase: BuildPhase.INSTALL_PODS,
|
||||
regexp: ({ env }: ErrorContext) =>
|
||||
env.EAS_BUILD_COCOAPODS_CACHE_URL ? /Error installing/ : undefined,
|
||||
createError: () =>
|
||||
new TrackedBuildError(
|
||||
'COCOAPODS_CACHE_INSTALLING_POD_ERROR',
|
||||
`cocoapods: error installing a pod using internal cache instance`
|
||||
),
|
||||
},
|
||||
{
|
||||
platform: Platform.IOS,
|
||||
phase: BuildPhase.INSTALL_PODS,
|
||||
regexp: ({ env }: ErrorContext) =>
|
||||
env.EAS_BUILD_COCOAPODS_CACHE_URL ? /No podspec exists at path/ : undefined,
|
||||
createError: () =>
|
||||
new TrackedBuildError(
|
||||
'COCOAPODS_CACHE_NO_PODSPEC_EXISTS_AT_PATH_ERROR',
|
||||
`cocoapods: error fetching a podspec through internal cache instance`
|
||||
),
|
||||
},
|
||||
{
|
||||
platform: Platform.IOS,
|
||||
phase: BuildPhase.INSTALL_PODS,
|
||||
regexp: ({ env }: ErrorContext) =>
|
||||
env.EAS_BUILD_COCOAPODS_CACHE_URL
|
||||
? new RegExp(escapeRegExp(env.EAS_BUILD_COCOAPODS_CACHE_URL))
|
||||
: undefined,
|
||||
createError: () => new TrackedBuildError('COCOAPODS_CACHE_ERROR', `cocoapods: cache error`),
|
||||
},
|
||||
{
|
||||
phase: BuildPhase.INSTALL_PODS,
|
||||
// [!] Invalid `Podfile` file: uninitialized constant Pod::Podfile::FlipperConfiguration.
|
||||
regexp: /\[!\] Invalid `Podfile` file/,
|
||||
createError: () => new TrackedBuildError('INVALID_PODFILE', 'pod: Invalid Podfile file.'),
|
||||
},
|
||||
{
|
||||
phase: BuildPhase.INSTALL_DEPENDENCIES,
|
||||
// info There appears to be trouble with your network connection. Retrying...
|
||||
regexp: /info There appears to be trouble with your network connection. Retrying/,
|
||||
createError: () =>
|
||||
new TrackedBuildError(
|
||||
'YARN_INSTALL_TROUBLE_WITH_NETWORK_CONNECTION',
|
||||
'yarn: There appears to be trouble with your network connection'
|
||||
),
|
||||
},
|
||||
{
|
||||
phase: BuildPhase.RUN_GRADLEW,
|
||||
platform: Platform.ANDROID,
|
||||
// Android Gradle plugin requires Java 11 to run. You are currently using Java 1.8
|
||||
regexp: /Android Gradle plugin requires Java .* to run. You are currently using Java/,
|
||||
createError: () =>
|
||||
new TrackedBuildError('INCOMPATIBLE_JAVA_VERSION', 'gradle: Incompatible java version.'),
|
||||
},
|
||||
{
|
||||
phase: BuildPhase.RUN_GRADLEW,
|
||||
platform: Platform.ANDROID,
|
||||
// /home/expo/workingdir/build/android/app/src/main/AndroidManifest.xml:27:9-33:20 Error:
|
||||
// android:exported needs to be explicitly specified for element <activity#androidx.test.core.app.InstrumentationActivityInvoker$EmptyActivity>. Apps targeting Android 12 and higher are required to specify an explicit value for `android:exported` when the corresponding component has an intent filter defined. See https://developer.android.com/guide/topics/manifest/activity-element#exported for details.
|
||||
regexp:
|
||||
/Apps targeting Android 12 and higher are required to specify an explicit value for `android:exported`/,
|
||||
createError: () =>
|
||||
new TrackedBuildError(
|
||||
'REQUIRE_EXPLICIT_EXPORTED_ANDROID_12',
|
||||
'Apps targeting Android 12 and higher are required to specify an explicit value for `android:exported`.'
|
||||
),
|
||||
},
|
||||
{
|
||||
phase: BuildPhase.RUN_GRADLEW,
|
||||
platform: Platform.ANDROID,
|
||||
// > A failure occurred while executing com.android.build.gradle.internal.res.Aapt2CompileRunnable
|
||||
// > Android resource compilation failed
|
||||
// ERROR:/home/expo/workingdir/build/android/app/src/main/res/mipmap-mdpi/ic_launcher.png: AAPT: error: failed to read PNG signature: file does not start with PNG signature.
|
||||
regexp: /AAPT: error: failed to read PNG signature: file does not start with PNG signature/,
|
||||
createError: () =>
|
||||
new TrackedBuildError('INVALID_PNG_SIGNATURE', 'gradle: Invalid PNG signature.'),
|
||||
},
|
||||
{
|
||||
phase: BuildPhase.RUN_GRADLEW,
|
||||
platform: Platform.ANDROID,
|
||||
// Execution failed for task ':app:processReleaseGoogleServices'.
|
||||
// > Malformed root json
|
||||
regexp: /Execution failed for task ':app:process.*GoogleServices'.*\s.*Malformed root json/,
|
||||
createError: () =>
|
||||
new TrackedBuildError(
|
||||
'GRADLE_MALFORMED_GOOGLE_SERVICES_JSON',
|
||||
'gradle: Malformed google-services.json.'
|
||||
),
|
||||
},
|
||||
{
|
||||
phase: BuildPhase.RUN_GRADLEW,
|
||||
platform: Platform.ANDROID,
|
||||
// Execution failed for task ':app:processDebugGoogleServices'.
|
||||
// > Missing project_info object
|
||||
regexp:
|
||||
/Execution failed for task ':app:process.*GoogleServices'.*\s.*Missing project_info object/,
|
||||
createError: () =>
|
||||
new TrackedBuildError(
|
||||
'GRADLE_MALFORMED_GOOGLE_SERVICES_JSON',
|
||||
'gradle: Missing project_info object.'
|
||||
),
|
||||
},
|
||||
{
|
||||
phase: BuildPhase.RUN_GRADLEW,
|
||||
platform: Platform.ANDROID,
|
||||
// Execution failed for task ':app:bundleReleaseJsAndAssets'.
|
||||
// > Process 'command 'node'' finished with non-zero exit value 1
|
||||
regexp:
|
||||
/Execution failed for task ':app:bundleReleaseJsAndAssets'.*\s.*Process 'command 'node'' finished with non-zero exit value/,
|
||||
createError: () =>
|
||||
new TrackedBuildError(
|
||||
'GRADLE_BUILD_BUNDLER_ERROR',
|
||||
"gradle: ':app:bundleReleaseJsAndAssets' failed."
|
||||
),
|
||||
},
|
||||
{
|
||||
platform: Platform.ANDROID,
|
||||
phase: BuildPhase.RUN_GRADLEW,
|
||||
// > Could not resolve org.jetbrains.kotlin:kotlin-annotation-processing-gradle:1.5.10.
|
||||
// Required by:
|
||||
// project :expo-updates
|
||||
// > Could not resolve org.jetbrains.kotlin:kotlin-annotation-processing-gradle:1.5.10.
|
||||
// > Could not get resource 'https://lsdkfjsdlkjf.com/android/releases/org/jetbrains/kotlin/kotlin-annotation-processing-gradle/1.5.10/kotlin-annotation-processing-gradle-1.5.10.pom'.
|
||||
// > Could not HEAD 'https://slkdfjldskjfl.com/android/releases/org/jetbrains/kotlin/kotlin-annotation-processing-gradle/1.5.10/kotlin-annotation-processing-gradle-1.5.10.pom'.
|
||||
// > Connect to sdlkfjsdlkf.com:443 [slkdfjdslk.com/38.178.101.5] failed: connect timed out
|
||||
regexp: /Could not get resource.*\n.*Could not HEAD 'https/,
|
||||
createError: () =>
|
||||
new TrackedBuildError('MAVEN_REGISTRY_CONNECTION_ERROR', `maven: registry connection error`),
|
||||
},
|
||||
{
|
||||
platform: Platform.ANDROID,
|
||||
phase: BuildPhase.RUN_GRADLEW,
|
||||
regexp: ({ env }: ErrorContext) =>
|
||||
env.EAS_BUILD_MAVEN_CACHE_URL
|
||||
? // The BlurView 2.0.3 jar was removed from jitpack.io
|
||||
// and it caused false positive MAVEN_CACHE_ERROR errors being reported.
|
||||
new RegExp(
|
||||
`^(?!.*Could not find BlurView-version-2\\.0\\.3\\.jar).*${escapeRegExp(
|
||||
env.EAS_BUILD_MAVEN_CACHE_URL
|
||||
)}`
|
||||
)
|
||||
: undefined,
|
||||
createError: () => new TrackedBuildError('MAVEN_CACHE_ERROR', `maven: cache error`),
|
||||
},
|
||||
{
|
||||
phase: BuildPhase.RUN_FASTLANE,
|
||||
platform: Platform.IOS,
|
||||
// error: exportArchive: exportOptionsPlist error for key "iCloudContainerEnvironment": expected one of {Development, Production}, but no value was provided
|
||||
regexp: /exportArchive: exportOptionsPlist error for key "iCloudContainerEnvironment"/,
|
||||
createError: () =>
|
||||
new TrackedBuildError(
|
||||
'MISSING_ICLOUD_CONTAINER_ENVIRONMENT',
|
||||
'fastlane: Missing iCloudContainerEnvironment in exportOptionsPlist.'
|
||||
),
|
||||
},
|
||||
{
|
||||
phase: BuildPhase.RUN_FASTLANE,
|
||||
platform: Platform.IOS,
|
||||
// The following build commands failed:
|
||||
// PhaseScriptExecution [CP-User]\ Generate\ app.manifest\ for\ expo-updates /Users/expo/Library/Developer/Xcode/DerivedData/Kenkohub-eqseedlxbgrzjqagscbclhbtstwh/Build/Intermediates.noindex/ArchiveIntermediates/Kenkohub/IntermediateBuildFilesPath/Pods.build/Release-iphoneos/EXUpdates.build/Script-BB6B5FD28815C045A20B2E5E3FEEBD6E.sh (in target 'EXUpdates' from project 'Pods')
|
||||
regexp:
|
||||
/The following build commands failed.*\s.*\[CP-User\]\\ Generate\\ app\.manifest\\ for\\ expo-updates/,
|
||||
createError: () =>
|
||||
new TrackedBuildError(
|
||||
'XCODE_BUILD_UPDATES_PHASE_SCRIPT',
|
||||
'fastlane: Generating app.manifest for expo-updates failed.'
|
||||
),
|
||||
},
|
||||
{
|
||||
phase: BuildPhase.RUN_FASTLANE,
|
||||
platform: Platform.IOS,
|
||||
// The following build commands failed:
|
||||
// CompileC /Users/expo/Library/Developer/Xcode/DerivedData/Docent-amxxhphjfdtkpxecgidgzvwvnvtc/Build/Intermediates.noindex/ArchiveIntermediates/Docent/IntermediateBuildFilesPath/Pods.build/Release-iphoneos/Flipper-Folly.build/Objects-normal/arm64/SSLErrors.o /Users/expo/workingdir/build/ios/Pods/Flipper-Folly/folly/io/async/ssl/SSLErrors.cpp normal arm64 c++ com.apple.compilers.llvm.clang.1_0.compiler (in target 'Flipper-Folly' from project 'Pods')
|
||||
regexp: /in target 'Flipper-Folly' from project 'Pods'/,
|
||||
createError: () =>
|
||||
new TrackedBuildError(
|
||||
'FLIPPER_FOLLY_COMPILE_ERROR',
|
||||
'fastlane: Flipper-Folly compile error.'
|
||||
),
|
||||
},
|
||||
{
|
||||
phase: BuildPhase.RUN_FASTLANE,
|
||||
platform: Platform.IOS,
|
||||
// The following build commands failed:
|
||||
// PhaseScriptExecution Bundle\ React\ Native\ code\ and\ images /Users/expo/Library/Developer/Xcode/DerivedData/cnaxwpahkhcjluhigkcwrturapmm/Build/Intermediates.noindex/ArchiveIntermediates/Test/IntermediateBuildFilesPath/Test.build/Release-iphoneos/Test.build/Script-00DD1BFF151E006B06BC.sh (in target 'Test' from project 'Test')
|
||||
regexp:
|
||||
/The following build commands failed.*\s.*PhaseScriptExecution Bundle\\ React\\ Native\\ code\\ and\\ images \/Users\/expo/,
|
||||
createError: () =>
|
||||
new TrackedBuildError(
|
||||
'XCODE_BUILD_BUNDLER_ERROR',
|
||||
'fastlane: Bundle React Native code and images failed.'
|
||||
),
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,106 @@
|
||||
import { BuildPhase, errors } from '@expo/eas-build-job';
|
||||
import fs from 'fs-extra';
|
||||
|
||||
import { buildErrorHandlers } from './buildErrorHandlers';
|
||||
import { ErrorContext, ErrorHandler, XCODE_BUILD_PHASE } from './errors.types';
|
||||
import { userErrorHandlers } from './userErrorHandlers';
|
||||
import { findXcodeBuildLogsPathAsync } from '../ios/xcodeBuildLogs';
|
||||
|
||||
async function maybeReadXcodeBuildLogs(
|
||||
phase: BuildPhase,
|
||||
buildLogsDirectory: string
|
||||
): Promise<string | undefined> {
|
||||
if (phase !== BuildPhase.RUN_FASTLANE) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const xcodeBuildLogsPath = await findXcodeBuildLogsPathAsync(buildLogsDirectory);
|
||||
|
||||
if (!xcodeBuildLogsPath) {
|
||||
return;
|
||||
}
|
||||
|
||||
return await fs.readFile(xcodeBuildLogsPath, 'utf-8');
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function resolveError<TError extends Error>(
|
||||
errorHandlers: ErrorHandler<TError>[],
|
||||
logLines: string[],
|
||||
errorContext: ErrorContext,
|
||||
xcodeBuildLogs?: string
|
||||
): TError | undefined {
|
||||
const { job, phase } = errorContext;
|
||||
const { platform } = job;
|
||||
const logs = logLines.join('\n');
|
||||
const handlers = errorHandlers
|
||||
.filter(handler => handler.platform === platform || !handler.platform)
|
||||
.filter(
|
||||
handler =>
|
||||
(handler.phase === XCODE_BUILD_PHASE && phase === BuildPhase.RUN_FASTLANE) ||
|
||||
handler.phase === phase ||
|
||||
!handler.phase
|
||||
)
|
||||
.filter(handler => ('mode' in job && handler.mode === job.mode) || !handler.mode);
|
||||
|
||||
for (const handler of handlers) {
|
||||
const regexp =
|
||||
typeof handler.regexp === 'function' ? handler.regexp(errorContext) : handler.regexp;
|
||||
if (!regexp) {
|
||||
continue;
|
||||
}
|
||||
const match =
|
||||
handler.phase === XCODE_BUILD_PHASE ? xcodeBuildLogs?.match(regexp) : logs.match(regexp);
|
||||
|
||||
if (match) {
|
||||
return handler.createError(match, errorContext);
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export async function resolveBuildPhaseErrorAsync(
|
||||
error: any,
|
||||
logLines: string[],
|
||||
errorContext: ErrorContext,
|
||||
buildLogsDirectory: string
|
||||
): Promise<errors.BuildError> {
|
||||
const { phase } = errorContext;
|
||||
if (error instanceof errors.BuildError) {
|
||||
return error;
|
||||
}
|
||||
const xcodeBuildLogs = await maybeReadXcodeBuildLogs(phase, buildLogsDirectory);
|
||||
const userFacingError =
|
||||
error instanceof errors.UserFacingError
|
||||
? error
|
||||
: resolveError(userErrorHandlers, logLines, errorContext, xcodeBuildLogs) ??
|
||||
new errors.UnknownError(errorContext.phase);
|
||||
const buildError = resolveError(buildErrorHandlers, logLines, errorContext, xcodeBuildLogs);
|
||||
|
||||
const isUnknownUserError =
|
||||
!userFacingError ||
|
||||
(
|
||||
[
|
||||
errors.ErrorCode.UNKNOWN_ERROR,
|
||||
errors.ErrorCode.UNKNOWN_GRADLE_ERROR,
|
||||
errors.ErrorCode.UNKNOWN_FASTLANE_ERROR,
|
||||
] as string[]
|
||||
).includes(userFacingError.errorCode);
|
||||
const message =
|
||||
(isUnknownUserError ? buildError?.message : userFacingError.message) ?? userFacingError.message;
|
||||
const errorCode =
|
||||
(isUnknownUserError ? buildError?.errorCode : userFacingError.errorCode) ??
|
||||
userFacingError.errorCode;
|
||||
|
||||
return new errors.BuildError(message, {
|
||||
errorCode,
|
||||
userFacingErrorCode: userFacingError.errorCode,
|
||||
userFacingMessage: userFacingError.message,
|
||||
docsUrl: userFacingError.docsUrl,
|
||||
innerError: error,
|
||||
buildPhase: phase,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { BuildMode, BuildPhase, Env, Job, Platform } from '@expo/eas-build-job';
|
||||
|
||||
export interface ErrorContext<TJob extends Job = Job> {
|
||||
phase: BuildPhase;
|
||||
job: TJob;
|
||||
env: Env;
|
||||
}
|
||||
|
||||
export const XCODE_BUILD_PHASE = 'XCODE_BUILD';
|
||||
|
||||
export interface ErrorHandler<T extends Error> {
|
||||
regexp: RegExp | ((ctx: ErrorContext) => RegExp | undefined);
|
||||
platform?: Platform;
|
||||
phase?: BuildPhase | typeof XCODE_BUILD_PHASE;
|
||||
mode?: BuildMode;
|
||||
createError: (matchResult: RegExpMatchArray, errCtx: ErrorContext) => T | undefined;
|
||||
}
|
||||
@@ -0,0 +1,311 @@
|
||||
import { BuildMode, BuildPhase, Platform, Workflow, errors } from '@expo/eas-build-job';
|
||||
|
||||
import { ErrorHandler, XCODE_BUILD_PHASE } from './errors.types';
|
||||
|
||||
import UserFacingError = errors.UserFacingError;
|
||||
|
||||
export const userErrorHandlers: ErrorHandler<UserFacingError>[] = [
|
||||
{
|
||||
platform: Platform.IOS,
|
||||
phase: BuildPhase.INSTALL_PODS,
|
||||
regexp: /requires CocoaPods version/,
|
||||
// example log:
|
||||
// [!] `React` requires CocoaPods version `>= 1.10.1`, which is not satisfied by your current version, `1.10.0`.
|
||||
createError: () =>
|
||||
new UserFacingError(
|
||||
'EAS_BUILD_UNSUPPORTED_COCOAPODS_VERSION_ERROR',
|
||||
`Your project requires a newer version of CocoaPods. You can update it in the build profile in eas.json by either:
|
||||
- changing the current version under key "cocoapods"
|
||||
- switching to an image that supports that version under key "image"`,
|
||||
'https://docs.expo.dev/build-reference/eas-json/'
|
||||
),
|
||||
},
|
||||
{
|
||||
platform: Platform.IOS,
|
||||
phase: BuildPhase.RUN_FASTLANE,
|
||||
regexp: /Could not find 'bundler' (.*) required by your/,
|
||||
// example log:
|
||||
// /System/Library/Frameworks/Ruby.framework/Versions/2.6/usr/lib/ruby/2.6.0/rubygems/dependency.rb:313:in `to_specs': Could not find 'bundler' (2.2.3) required by your /Users/expo/project/build/ios/Gemfile.lock. (Gem::MissingSpecVersionError)
|
||||
createError: () =>
|
||||
new UserFacingError(
|
||||
'EAS_BUILD_UNSUPPORTED_BUNDLER_VERSION_ERROR',
|
||||
`Your project requires a different version of the Ruby "bundler" program than the version installed in this EAS Build environment. You can specify which version of "bundler" to install by specifying the version under "build"→[buildProfileName]→"ios"→"bundler" in eas.json.`,
|
||||
'https://docs.expo.dev/build-reference/eas-json/'
|
||||
),
|
||||
},
|
||||
{
|
||||
platform: Platform.ANDROID,
|
||||
phase: BuildPhase.RUN_GRADLEW,
|
||||
// example log:
|
||||
// > Failed to read key keyalias from store "/build/workingdir/build/generic/keystore-5787e6af-3002-4cb7-8a57-3e73d13313c2.jks": Invalid keystore format
|
||||
regexp: /Invalid keystore format/,
|
||||
createError: () =>
|
||||
new UserFacingError(
|
||||
'EAS_BUILD_INVALID_KEYSTORE_FORMAT_ERROR',
|
||||
'The keystore used in this build is malformed or it has an unsupported type. Make sure you provided the correct file.'
|
||||
),
|
||||
},
|
||||
{
|
||||
platform: Platform.ANDROID,
|
||||
phase: BuildPhase.RUN_GRADLEW,
|
||||
// example log:
|
||||
// > Failed to read key keyalias from store "/build/workingdir/build/generic/keystore-286069a8-4bb9-48a6-add9-acf6b58ea06d.jks": null
|
||||
regexp: /Failed to read key[^\n]+from store/,
|
||||
createError: () =>
|
||||
new UserFacingError(
|
||||
'EAS_BUILD_INVALID_KEYSTORE_ALIAS_ERROR',
|
||||
'The alias specified for this keystore does not exist. Make sure you specified the correct value.'
|
||||
),
|
||||
},
|
||||
{
|
||||
platform: Platform.ANDROID,
|
||||
phase: BuildPhase.PREBUILD,
|
||||
// example log:
|
||||
// [15:42:05] Error: Cannot copy google-services.json from /build/workingdir/build/managed/abc to /build/workingdir/build/managed/android/app/google-services.json
|
||||
// or
|
||||
// [11:17:29] [android.dangerous]: withAndroidDangerousBaseMod: Cannot copy google-services.json from /home/expo/workingdir/build/test/test-google-services.json to /home/expo/workingdir/build/android/app/google-services.json. Please make sure the source and destination paths exist.
|
||||
// [11:17:29] Error: [android.dangerous]: withAndroidDangerousBaseMod: Cannot copy google-services.json from /home/expo/workingdir/build/test/test-google-services.json to /home/expo/workingdir/build/android/app/google-services.json. Please make sure the source and destination paths exist.
|
||||
regexp: /Cannot copy google-services\.json/,
|
||||
createError: () =>
|
||||
new UserFacingError(
|
||||
'EAS_BUILD_MISSING_GOOGLE_SERVICES_JSON_ERROR',
|
||||
'"google-services.json" is missing, make sure that the file exists. Remember that EAS Build only uploads the files tracked by git. Use EAS environment variables to provide EAS Build with the file.',
|
||||
'https://docs.expo.dev/eas/environment-variables/#file-environment-variables'
|
||||
),
|
||||
},
|
||||
{
|
||||
platform: Platform.ANDROID,
|
||||
phase: BuildPhase.RUN_GRADLEW,
|
||||
// Execution failed for task ':app:processReleaseGoogleServices'.
|
||||
// > File google-services.json is missing. The Google Services Plugin cannot function without it.
|
||||
// Searched Location:
|
||||
regexp:
|
||||
/File google-services\.json is missing\. The Google Services Plugin cannot function without it/,
|
||||
createError: () =>
|
||||
new UserFacingError(
|
||||
'EAS_BUILD_MISSING_GOOGLE_SERVICES_JSON_ERROR',
|
||||
'"google-services.json" is missing, make sure that the file exists. Remember that EAS Build only uploads the files tracked by git. Use EAS environment variables to provide EAS Build with the file.',
|
||||
'https://docs.expo.dev/eas/environment-variables/#file-environment-variables'
|
||||
),
|
||||
},
|
||||
{
|
||||
platform: Platform.IOS,
|
||||
phase: BuildPhase.PREBUILD,
|
||||
// example log:
|
||||
// [08:44:18] ENOENT: no such file or directory, copyfile '/Users/expo/workingdir/build/managed/abc' -> '/Users/expo/workingdir/build/managed/ios/testapp/GoogleService-Info.plist'
|
||||
regexp: /ENOENT: no such file or directory, copyfile .*GoogleService-Info.plist/,
|
||||
createError: () =>
|
||||
new UserFacingError(
|
||||
'EAS_BUILD_MISSING_GOOGLE_SERVICES_PLIST_ERROR',
|
||||
'"GoogleService-Info.plist" is missing, make sure that the file exists. Remember that EAS Build only uploads the files tracked by git. Use EAS environment variables to provide EAS Build with the file.',
|
||||
'https://docs.expo.dev/eas/environment-variables/#file-environment-variables'
|
||||
),
|
||||
},
|
||||
{
|
||||
platform: Platform.IOS,
|
||||
phase: BuildPhase.INSTALL_PODS,
|
||||
// example log:
|
||||
// [!] CocoaPods could not find compatible versions for pod "react-native-google-maps"
|
||||
// In Podfile:
|
||||
// react-native-google-maps (from `/Users/expo/workingdir/build/node_modules/react-native-maps`)
|
||||
// Specs satisfying the `react-native-google-maps (from `/Users/expo/workingdir/build/node_modules/react-native-maps`)` dependency were found, but they required a higher minimum deployment target.
|
||||
// Error: Compatible versions of some pods could not be resolved.
|
||||
regexp:
|
||||
/Specs satisfying the `(.*)` dependency were found, but they required a higher minimum deployment target/,
|
||||
createError: (_, { job }) => {
|
||||
return new UserFacingError(
|
||||
'EAS_BUILD_HIGHER_MINIMUM_DEPLOYMENT_TARGET_ERROR',
|
||||
`Some pods require a higher minimum deployment target.
|
||||
${
|
||||
'type' in job && job.type === Workflow.MANAGED
|
||||
? 'You can use the expo-build-properties config plugin (https://docs.expo.dev/versions/latest/sdk/build-properties/) to override the default native build properties and set a different minimum deployment target.'
|
||||
: 'You need to manually update the minimum deployment target in your project to resolve this issue.'
|
||||
}
|
||||
`
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
platform: Platform.IOS,
|
||||
phase: BuildPhase.INSTALL_PODS,
|
||||
// example log:
|
||||
// [!] CocoaPods could not find compatible versions for pod "Firebase/Core":
|
||||
// In snapshot (Podfile.lock):
|
||||
// Firebase/Core (= 6.14.0)
|
||||
// In Podfile:
|
||||
// EXFirebaseCore (from `../node_modules/expo-firebase-core/ios`) was resolved to 3.0.0, which depends on
|
||||
// Firebase/Core (= 7.7.0)
|
||||
// You have either:
|
||||
// * out-of-date source repos which you can update with `pod repo update` or with `pod install --repo-update`.
|
||||
// * changed the constraints of dependency `Firebase/Core` inside your development pod `EXFirebaseCore`.
|
||||
// You should run `pod update Firebase/Core` to apply changes you've made.
|
||||
regexp: /CocoaPods could not find compatible versions for pod /,
|
||||
createError: () => {
|
||||
return new UserFacingError(
|
||||
'EAS_BUILD_INCOMPATIBLE_PODS_ERROR',
|
||||
`Compatible versions of some pods could not be resolved.
|
||||
You are seeing this error because either:
|
||||
- Some of the pods used in your project depend on different versions of the same pod. See logs for more information.
|
||||
- If you are caching Podfile.lock using "cache" field in eas.json, then versions there might not match required values in Podspecs of some installed libraries. To fix this, you can re-run build command with "--clear-cache" option, or select "Clear cache and retry build" on the build page.
|
||||
`
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
phase: BuildPhase.INSTALL_DEPENDENCIES,
|
||||
// example log:
|
||||
// [stderr] npm ERR! Fix the upstream dependency conflict, or retry
|
||||
// [stderr] npm ERR! this command with --force, or --legacy-peer-deps
|
||||
// [stderr] npm ERR! to accept an incorrect (and potentially broken) dependency resolution.
|
||||
regexp:
|
||||
/Fix the upstream dependency conflict, or retry.*\s.*this command with --force, or --legacy-peer-deps/,
|
||||
createError: (matchResult: RegExpMatchArray) => {
|
||||
if (matchResult.length >= 2) {
|
||||
return new UserFacingError(
|
||||
'EAS_BUILD_NPM_CONFLICTING_PEER_DEPENDENCIES',
|
||||
`Some of your peer dependencies are not compatible. The recommended approach is to fix your dependencies by resolving any conflicts listed by "npm install". As a temporary workaround you can:
|
||||
- Add ".npmrc" file with "legacy-peer-deps=true" and commit that to your repo.
|
||||
- Delete package-lock.json and use yarn instead. It does not enforce peer dependencies.
|
||||
- Downgrade to older version of npm on EAS Build, by adding "npm install -g npm@version" in "eas-build-pre-install" script in package.json.`
|
||||
);
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
},
|
||||
{
|
||||
phase: BuildPhase.INSTALL_DEPENDENCIES,
|
||||
// example log:
|
||||
// [stderr] error https://github.com/expo/react-native/archive/sdk-41.0.0.tar.gz: Integrity check failed for "react-native" (computed integrity doesn't match our records, got "sha512-3jHI2YufrJi7eIABRf/DN/I2yOkmIZ0vAyezTz+PAUJiEs4v//5LLojWEU+W53AZsnuaEMcl/4fVy4bd+OuUbA== sha1-o9QuQTXIkc8VozXPaZIullB9a40=")
|
||||
regexp: /Integrity check failed for "(.*)" \(computed integrity doesn't match our records, got/,
|
||||
createError: (matchResult: RegExpMatchArray) => {
|
||||
if (matchResult.length >= 2) {
|
||||
return new UserFacingError(
|
||||
'EAS_BUILD_YARN_LOCK_CHECKSUM_ERROR',
|
||||
`Checksum for package "${matchResult[1]}" does not match value in registry. To fix that:
|
||||
- run "yarn cache clean"
|
||||
- remove yarn.lock (or only the section for that package)
|
||||
- run "yarn install --force"`
|
||||
);
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
},
|
||||
{
|
||||
phase: BuildPhase.INSTALL_DEPENDENCIES,
|
||||
// example log:
|
||||
// yarn install v1.22.17
|
||||
// [1/4] Resolving packages...
|
||||
// [2/4] Fetching packages...
|
||||
// [1/4] Resolving packages...
|
||||
// [2/4] Fetching packages...
|
||||
// [stderr] error https://registry.yarnpkg.com/jest-util/-/jest-util-26.6.2.tgz: Extracting tar content of undefined failed, the file appears to be corrupt: "ENOENT: no such file or directory, chmod '/Users/expo/Library/Caches/Yarn/v6/npm-jest-util-26.6.2-907535dbe4d5a6cb4c47ac9b926f6af29576cbc1-integrity/node_modules/jest-util/build/pluralize.d.ts'"
|
||||
regexp:
|
||||
/\[1\/4\] Resolving packages...\s*\[2\/4\] Fetching packages...\s*\[1\/4\] Resolving packages...\s*\[2\/4\] Fetching packages.../,
|
||||
createError: (matchResult: RegExpMatchArray) => {
|
||||
if (matchResult) {
|
||||
return new UserFacingError(
|
||||
'EAS_BUILD_YARN_MULTIPLE_INSTANCES_ERROR',
|
||||
`One of project dependencies is starting new install process while the main one is still in progress, which might result in corrupted packages. Most likely the reason for error is "prepare" script in git-referenced dependency of your project. Learn more: https://github.com/yarnpkg/yarn/issues/7212#issuecomment-493720324`
|
||||
);
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
},
|
||||
{
|
||||
platform: Platform.IOS,
|
||||
phase: XCODE_BUILD_PHASE,
|
||||
// Prepare packages
|
||||
// Computing target dependency graph and provisioning inputs
|
||||
// Create build description
|
||||
// Build description signature: 33a5c28977280822abe5e7bd7fe02529
|
||||
// Build description path: /Users/expo/Library/Developer/Xcode/DerivedData/testapp-fazozgerxcvvfifkipojsjftgyih/Build/Intermediates.noindex/ArchiveIntermediates/testapp/IntermediateBuildFilesPath/XCBuildData/33a5c28977280822abe5e7bd7fe02529-desc.xcbuild
|
||||
// note: Building targets in dependency order
|
||||
// /Users/expo/workingdir/build/managed/ios/Pods/Pods.xcodeproj: error: Signing for "EXConstants-EXConstants" requires a development team. Select a development team in the Signing & Capabilities editor. (in target 'EXConstants-EXConstants' from project 'Pods')
|
||||
// /Users/expo/workingdir/build/managed/ios/Pods/Pods.xcodeproj: error: Signing for "React-Core-AccessibilityResources" requires a development team. Select a development team in the Signing & Capabilities editor. (in target 'React-Core-AccessibilityResources' from project 'Pods')
|
||||
// warning: Run script build phase '[CP-User] Generate app.manifest for expo-updates' will be run during every build because it does not specify any outputs. To address this warning, either add output dependencies to the script phase, or configure it to run in every build by unchecking "Based on dependency analysis" in the script phase. (in target 'EXUpdates' from project 'Pods')
|
||||
// warning: Run script build phase 'Start Packager' will be run during every build because it does not specify any outputs. To address this warning, either add output dependencies to the script phase, or configure it to run in every build by unchecking "Based on dependency analysis" in the script phase. (in target 'testapp' from project 'testapp')
|
||||
// warning: Run script build phase 'Bundle React Native code and images' will be run during every build because it does not specify any outputs. To address this warning, either add output dependencies to the script phase, or configure it to run in every build by unchecking "Based on dependency analysis" in the script phase. (in target 'testapp' from project 'testapp')
|
||||
// warning: Run script build phase '[CP-User] Generate app.config for prebuilt Constants.manifest' will be run during every build because it does not specify any outputs. To address this warning, either add output dependencies to the script phase, or configure it to run in every build by unchecking "Based on dependency analysis" in the script phase. (in target 'EXConstants' from project 'Pods')
|
||||
// /Users/expo/workingdir/build/managed/ios/Pods/Pods.xcodeproj: error: Signing for "EXUpdates-EXUpdates" requires a development team. Select a development team in the Signing & Capabilities editor. (in target 'EXUpdates-EXUpdates' from project 'Pods')
|
||||
regexp: /error: Signing for "[a-zA-Z-0-9_]+" requires a development team/,
|
||||
createError: (_, { job }) =>
|
||||
'type' in job && job.type === Workflow.MANAGED
|
||||
? new UserFacingError(
|
||||
'XCODE_RESOURCE_BUNDLE_CODE_SIGNING_ERROR',
|
||||
`Starting from Xcode 14, resource bundles are signed by default, which requires setting the development team for each resource bundle target.
|
||||
To resolve this issue, downgrade to an older Xcode version using the "image" field in eas.json, or upgrade to SDK 46 or higher.`,
|
||||
'https://docs.expo.dev/build-reference/infrastructure/#ios-build-server-configurations'
|
||||
)
|
||||
: new UserFacingError(
|
||||
'XCODE_RESOURCE_BUNDLE_CODE_SIGNING_ERROR',
|
||||
`Starting from Xcode 14, resource bundles are signed by default, which requires setting the development team for each resource bundle target.
|
||||
To resolve this issue, downgrade to an older Xcode version using the "image" field in eas.json, or turn off signing resource bundles in your Podfile: https://expo.fyi/r/disable-bundle-resource-signing`,
|
||||
'https://docs.expo.dev/build-reference/infrastructure/#ios-build-server-configurations'
|
||||
),
|
||||
},
|
||||
{
|
||||
platform: Platform.ANDROID,
|
||||
phase: BuildPhase.RUN_GRADLEW,
|
||||
regexp: /.*/,
|
||||
createError: () =>
|
||||
new UserFacingError(
|
||||
errors.ErrorCode.UNKNOWN_GRADLE_ERROR,
|
||||
'Gradle build failed with unknown error. See logs for the "Run gradlew" phase for more information.'
|
||||
),
|
||||
},
|
||||
{
|
||||
platform: Platform.IOS,
|
||||
phase: BuildPhase.RUN_FASTLANE,
|
||||
mode: BuildMode.RESIGN,
|
||||
regexp: /No provisioning profile for application: '(.+)' with bundle identifier '(.+)'/,
|
||||
createError: () =>
|
||||
new UserFacingError(
|
||||
'EAS_BUILD_RESIGN_PROVISIONING_PROFILE_MISMATCH_ERROR',
|
||||
`The bundle identifier in provisioning profile used to resign the app does not match the bundle identifier of the app selected to be resigned. See logs above for more information.`
|
||||
),
|
||||
},
|
||||
{
|
||||
platform: Platform.IOS,
|
||||
phase: BuildPhase.RUN_FASTLANE,
|
||||
mode: BuildMode.RESIGN,
|
||||
regexp: /.*/,
|
||||
createError: () =>
|
||||
new UserFacingError(
|
||||
errors.ErrorCode.UNKNOWN_FASTLANE_RESIGN_ERROR,
|
||||
`The "Run fastlane" step failed with an unknown error.`
|
||||
),
|
||||
},
|
||||
{
|
||||
platform: Platform.IOS,
|
||||
phase: XCODE_BUILD_PHASE,
|
||||
// MkDir /Users/expo/workingdir/build/packages/apps/NFLNetwork/ios/build/Build/Products/Release-iphonesimulator/expo-dev-launcher/EXDevLauncher.bundle (in target 'expo-dev-launcher-EXDevLauncher' from project 'Pods')
|
||||
// cd /Users/expo/workingdir/build/packages/apps/NFLNetwork/ios/Pods
|
||||
// /bin/mkdir -p /Users/expo/workingdir/build/packages/apps/NFLNetwork/ios/build/Build/Products/Release-iphonesimulator/expo-dev-launcher/EXDevLauncher.bundle
|
||||
// ProcessXCFramework /Users/expo/workingdir/build/packages/video/ios/Vendor/dependency/ProgrammaticAccessLibrary.xcframework /Users/expo/workingdir/build/packages/apps/NFLNetwork/ios/build/Build/Products/Release-iphonesimulator/ProgrammaticAccessLibrary.framework ios simulator
|
||||
// cd /Users/expo/workingdir/build/packages/apps/NFLNetwork/ios
|
||||
// builtin-process-xcframework --xcframework /Users/expo/workingdir/build/packages/video/ios/Vendor/dependency/ProgrammaticAccessLibrary.xcframework --platform ios --environment simulator --target-path /Users/expo/workingdir/build/packages/apps/NFLNetwork/ios/build/Build/Products/Release-iphonesimulator
|
||||
// /Users/expo/workingdir/build/packages/video/ios/Vendor/dependency/ProgrammaticAccessLibrary.xcframework:1:1: error: The signature of “ProgrammaticAccessLibrary.xcframework” cannot be verified.
|
||||
// note: A sealed resource is missing or invalid
|
||||
// note: /Users/expo/workingdir/build/packages/video/ios/Vendor/dependency/ProgrammaticAccessLibrary.xcframework: a sealed resource is missing or invalid
|
||||
// file modified: /Users/expo/workingdir/build/packages/video/ios/Vendor/dependency/ProgrammaticAccessLibrary.xcframework/ios-arm64_x86_64-simulator/ProgrammaticAccessLibrary.framework/ProgrammaticAccessLibrary
|
||||
// file modified: /Users/expo/workingdir/build/packages/video/ios/Vendor/dependency/ProgrammaticAccessLibrary.xcframework/ios-arm64/ProgrammaticAccessLibrary.framework/ProgrammaticAccessLibrary
|
||||
regexp: /error: .+/g,
|
||||
createError: matchResult =>
|
||||
new UserFacingError(
|
||||
'XCODE_BUILD_ERROR',
|
||||
`The "Run fastlane" step failed because of an error in the Xcode build process. We automatically detected following errors in your Xcode build logs:\n${matchResult
|
||||
.map(match => `- ${match.replace('error: ', '')}`)
|
||||
.join('\n')}\nRefer to "Xcode Logs" below for additional, more detailed logs.`
|
||||
),
|
||||
},
|
||||
{
|
||||
platform: Platform.IOS,
|
||||
phase: BuildPhase.RUN_FASTLANE,
|
||||
regexp: /.*/,
|
||||
createError: () =>
|
||||
new UserFacingError(
|
||||
errors.ErrorCode.UNKNOWN_FASTLANE_ERROR,
|
||||
`The "Run fastlane" step failed with an unknown error. Refer to "Xcode Logs" below for additional, more detailed logs.`
|
||||
),
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,77 @@
|
||||
import { BuildJob } from '@expo/eas-build-job';
|
||||
import { vol } from 'memfs';
|
||||
|
||||
import { createTestIosJob } from '../../__tests__/utils/job';
|
||||
import { createMockLogger } from '../../__tests__/utils/logger';
|
||||
import { prepareProjectSourcesAsync } from '../../common/projectSources';
|
||||
import { BuildContext } from '../../context';
|
||||
import { findAndUploadXcodeBuildLogsAsync } from '../../ios/xcodeBuildLogs';
|
||||
import { runCustomBuildAsync } from '../custom';
|
||||
|
||||
jest.mock('../../common/projectSources');
|
||||
jest.mock('../../ios/xcodeBuildLogs');
|
||||
|
||||
const findAndUploadXcodeBuildLogsAsyncMock = jest.mocked(findAndUploadXcodeBuildLogsAsync);
|
||||
|
||||
describe(runCustomBuildAsync, () => {
|
||||
let ctx: BuildContext<BuildJob>;
|
||||
|
||||
beforeEach(() => {
|
||||
const job = createTestIosJob();
|
||||
|
||||
jest.mocked(prepareProjectSourcesAsync).mockImplementation(async () => {
|
||||
vol.mkdirSync('/workingdir/env', { recursive: true });
|
||||
vol.mkdirSync('/workingdir/temporary-custom-build', { recursive: true });
|
||||
vol.fromJSON(
|
||||
{
|
||||
'test.yaml': `
|
||||
build:
|
||||
steps:
|
||||
- eas/checkout
|
||||
`,
|
||||
},
|
||||
'/workingdir/temporary-custom-build'
|
||||
);
|
||||
return { handled: true };
|
||||
});
|
||||
|
||||
ctx = new BuildContext(
|
||||
{
|
||||
...job,
|
||||
customBuildConfig: {
|
||||
path: 'test.yaml',
|
||||
},
|
||||
},
|
||||
{
|
||||
workingdir: '/workingdir',
|
||||
logBuffer: { getLogs: () => [], getPhaseLogs: () => [] },
|
||||
logger: createMockLogger(),
|
||||
env: {
|
||||
__API_SERVER_URL: 'http://api.expo.test',
|
||||
},
|
||||
uploadArtifact: jest.fn(),
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('calls findAndUploadXcodeBuildLogsAsync in an iOS job if its artifacts is empty', async () => {
|
||||
await runCustomBuildAsync(ctx);
|
||||
expect(findAndUploadXcodeBuildLogsAsyncMock).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not call findAndUploadXcodeBuildLogsAsync in an iOS job if artifacts is already present', async () => {
|
||||
ctx.artifacts.XCODE_BUILD_LOGS = 'uploaded';
|
||||
await runCustomBuildAsync(ctx);
|
||||
expect(findAndUploadXcodeBuildLogsAsyncMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('retries checking out the project', async () => {
|
||||
jest.mocked(prepareProjectSourcesAsync).mockImplementationOnce(async () => {
|
||||
throw new Error('Failed to clone repository');
|
||||
});
|
||||
|
||||
await runCustomBuildAsync(ctx);
|
||||
|
||||
expect(prepareProjectSourcesAsync).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,192 @@
|
||||
import { Android, BuildMode, BuildPhase, Workflow } from '@expo/eas-build-job';
|
||||
import nullthrows from 'nullthrows';
|
||||
import path from 'path';
|
||||
|
||||
import { runBuilderWithHooksAsync } from './common';
|
||||
import { runCustomBuildAsync } from './custom';
|
||||
import { restoreCredentials } from '../android/credentials';
|
||||
import {
|
||||
ensureLFLineEndingsInGradlewScript,
|
||||
resolveGradleCommand,
|
||||
runGradleCommand,
|
||||
} from '../android/gradle';
|
||||
import { configureBuildGradle } from '../android/gradleConfig';
|
||||
import { eagerBundleAsync, shouldUseEagerBundle } from '../common/eagerBundle';
|
||||
import { prebuildAsync } from '../common/prebuild';
|
||||
import { setupAsync } from '../common/setup';
|
||||
import { Artifacts, BuildContext, SkipNativeBuildError } from '../context';
|
||||
import { cacheStatsAsync, restoreCcacheAsync } from '../steps/functions/restoreBuildCache';
|
||||
import { saveCcacheAsync } from '../steps/functions/saveBuildCache';
|
||||
import { uploadApplicationArchive } from '../utils/artifacts';
|
||||
import {
|
||||
configureExpoUpdatesIfInstalledAsync,
|
||||
resolveRuntimeVersionForExpoUpdatesIfConfiguredAsync,
|
||||
} from '../utils/expoUpdates';
|
||||
import { Hook, runHookIfPresent } from '../utils/hooks';
|
||||
import { prepareExecutableAsync } from '../utils/prepareBuildExecutable';
|
||||
|
||||
export default async function androidBuilder(ctx: BuildContext<Android.Job>): Promise<Artifacts> {
|
||||
if (ctx.job.mode === BuildMode.BUILD) {
|
||||
await prepareExecutableAsync(ctx);
|
||||
return await runBuilderWithHooksAsync(ctx, buildAsync);
|
||||
} else if (ctx.job.mode === BuildMode.RESIGN) {
|
||||
throw new Error('Not implemented');
|
||||
} else if (ctx.job.mode === BuildMode.CUSTOM || ctx.job.mode === BuildMode.REPACK) {
|
||||
return await runCustomBuildAsync(ctx);
|
||||
} else {
|
||||
throw new Error('Not implemented');
|
||||
}
|
||||
}
|
||||
|
||||
async function buildAsync(ctx: BuildContext<Android.Job>): Promise<void> {
|
||||
await setupAsync(ctx);
|
||||
const evictUsedBefore = new Date();
|
||||
const workingDirectory = ctx.getReactNativeProjectDirectory();
|
||||
const hasNativeCode = ctx.job.type === Workflow.GENERIC;
|
||||
|
||||
if (hasNativeCode) {
|
||||
await ctx.runBuildPhase(BuildPhase.FIX_GRADLEW, async () => {
|
||||
await ensureLFLineEndingsInGradlewScript(ctx);
|
||||
});
|
||||
}
|
||||
|
||||
await ctx.runBuildPhase(BuildPhase.PREBUILD, async () => {
|
||||
if (hasNativeCode) {
|
||||
ctx.markBuildPhaseSkipped();
|
||||
ctx.logger.info(
|
||||
'Skipped running "expo prebuild" because the "android" directory already exists. Learn more about the build process: https://docs.expo.dev/build-reference/android-builds/'
|
||||
);
|
||||
return;
|
||||
}
|
||||
await prebuildAsync(ctx, {
|
||||
logger: ctx.logger,
|
||||
workingDir: ctx.getReactNativeProjectDirectory(),
|
||||
});
|
||||
});
|
||||
|
||||
await ctx.runBuildPhase(BuildPhase.RESTORE_CACHE, async () => {
|
||||
if (ctx.isLocal) {
|
||||
ctx.logger.info('Local builds do not support restoring cache');
|
||||
return;
|
||||
}
|
||||
await ctx.cacheManager?.restoreCache(ctx);
|
||||
await restoreCcacheAsync({
|
||||
logger: ctx.logger,
|
||||
workingDirectory,
|
||||
platform: ctx.job.platform,
|
||||
env: ctx.env,
|
||||
secrets: ctx.job.secrets,
|
||||
});
|
||||
});
|
||||
|
||||
await ctx.runBuildPhase(BuildPhase.POST_INSTALL_HOOK, async () => {
|
||||
await runHookIfPresent(ctx, Hook.POST_INSTALL);
|
||||
});
|
||||
|
||||
const resolvedExpoUpdatesRuntimeVersion = await ctx.runBuildPhase(
|
||||
BuildPhase.CALCULATE_EXPO_UPDATES_RUNTIME_VERSION,
|
||||
async () => {
|
||||
return await resolveRuntimeVersionForExpoUpdatesIfConfiguredAsync({
|
||||
cwd: ctx.getReactNativeProjectDirectory(),
|
||||
logger: ctx.logger,
|
||||
appConfig: ctx.appConfig,
|
||||
platform: ctx.job.platform,
|
||||
workflow: ctx.job.type,
|
||||
env: ctx.env,
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
if (
|
||||
nullthrows(ctx.job.secrets, 'Secrets must be defined for non-custom builds').buildCredentials
|
||||
) {
|
||||
await ctx.runBuildPhase(BuildPhase.PREPARE_CREDENTIALS, async () => {
|
||||
await restoreCredentials(ctx);
|
||||
await configureBuildGradle(ctx);
|
||||
});
|
||||
}
|
||||
await ctx.runBuildPhase(BuildPhase.CONFIGURE_EXPO_UPDATES, async () => {
|
||||
await configureExpoUpdatesIfInstalledAsync(ctx, {
|
||||
resolvedRuntimeVersion: resolvedExpoUpdatesRuntimeVersion?.runtimeVersion ?? null,
|
||||
resolvedFingerprintSources: resolvedExpoUpdatesRuntimeVersion?.fingerprintSources ?? null,
|
||||
});
|
||||
});
|
||||
|
||||
if (ctx.skipNativeBuild) {
|
||||
throw new SkipNativeBuildError('Skipping Gradle build');
|
||||
}
|
||||
|
||||
if (!ctx.env.EAS_BUILD_DISABLE_BUNDLE_JAVASCRIPT_STEP && shouldUseEagerBundle(ctx.metadata)) {
|
||||
await ctx.runBuildPhase(BuildPhase.EAGER_BUNDLE, async () => {
|
||||
await eagerBundleAsync({
|
||||
platform: ctx.job.platform,
|
||||
workingDir: ctx.getReactNativeProjectDirectory(),
|
||||
logger: ctx.logger,
|
||||
env: {
|
||||
...ctx.env,
|
||||
...(resolvedExpoUpdatesRuntimeVersion?.runtimeVersion
|
||||
? {
|
||||
EXPO_UPDATES_FINGERPRINT_OVERRIDE:
|
||||
resolvedExpoUpdatesRuntimeVersion?.runtimeVersion,
|
||||
EXPO_UPDATES_WORKFLOW_OVERRIDE: ctx.job.type,
|
||||
}
|
||||
: null),
|
||||
},
|
||||
packageManager: ctx.packageManager,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
await ctx.runBuildPhase(BuildPhase.RUN_GRADLEW, async () => {
|
||||
const gradleCommand = resolveGradleCommand(ctx.job);
|
||||
await runGradleCommand(ctx, {
|
||||
logger: ctx.logger,
|
||||
gradleCommand,
|
||||
androidDir: path.join(ctx.getReactNativeProjectDirectory(), 'android'),
|
||||
...(resolvedExpoUpdatesRuntimeVersion?.runtimeVersion
|
||||
? {
|
||||
extraEnv: {
|
||||
EXPO_UPDATES_FINGERPRINT_OVERRIDE: resolvedExpoUpdatesRuntimeVersion.runtimeVersion,
|
||||
EXPO_UPDATES_WORKFLOW_OVERRIDE: ctx.job.type,
|
||||
},
|
||||
}
|
||||
: null),
|
||||
});
|
||||
});
|
||||
|
||||
await ctx.runBuildPhase(BuildPhase.PRE_UPLOAD_ARTIFACTS_HOOK, async () => {
|
||||
await runHookIfPresent(ctx, Hook.PRE_UPLOAD_ARTIFACTS);
|
||||
});
|
||||
|
||||
await ctx.runBuildPhase(BuildPhase.UPLOAD_APPLICATION_ARCHIVE, async () => {
|
||||
await uploadApplicationArchive(ctx, {
|
||||
patternOrPath: ctx.job.applicationArchivePath ?? 'android/app/build/outputs/**/*.{apk,aab}',
|
||||
rootDir: ctx.getReactNativeProjectDirectory(),
|
||||
logger: ctx.logger,
|
||||
});
|
||||
});
|
||||
|
||||
await ctx.runBuildPhase(BuildPhase.SAVE_CACHE, async () => {
|
||||
if (ctx.isLocal) {
|
||||
ctx.logger.info('Local builds do not support saving cache.');
|
||||
return;
|
||||
}
|
||||
await ctx.cacheManager?.saveCache(ctx);
|
||||
await saveCcacheAsync({
|
||||
logger: ctx.logger,
|
||||
workingDirectory,
|
||||
platform: ctx.job.platform,
|
||||
evictUsedBefore,
|
||||
env: ctx.env,
|
||||
secrets: ctx.job.secrets,
|
||||
});
|
||||
});
|
||||
|
||||
await ctx.runBuildPhase(BuildPhase.CACHE_STATS, async () => {
|
||||
await cacheStatsAsync({
|
||||
logger: ctx.logger,
|
||||
env: ctx.env,
|
||||
secrets: ctx.job.secrets,
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import { BuildJob, BuildPhase, Ios, Platform } from '@expo/eas-build-job';
|
||||
|
||||
import { Artifacts, BuildContext } from '../context';
|
||||
import { findAndUploadXcodeBuildLogsAsync } from '../ios/xcodeBuildLogs';
|
||||
import { maybeFindAndUploadBuildArtifacts } from '../utils/artifacts';
|
||||
import { Hook, runHookIfPresent } from '../utils/hooks';
|
||||
|
||||
export async function runBuilderWithHooksAsync<T extends BuildJob>(
|
||||
ctx: BuildContext<T>,
|
||||
builderAsync: (ctx: BuildContext<T>) => Promise<void>
|
||||
): Promise<Artifacts> {
|
||||
try {
|
||||
let buildSuccess = true;
|
||||
try {
|
||||
await builderAsync(ctx);
|
||||
await ctx.runBuildPhase(BuildPhase.ON_BUILD_SUCCESS_HOOK, async () => {
|
||||
await runHookIfPresent(ctx, Hook.ON_BUILD_SUCCESS);
|
||||
});
|
||||
} catch (err: any) {
|
||||
buildSuccess = false;
|
||||
await ctx.runBuildPhase(BuildPhase.ON_BUILD_ERROR_HOOK, async () => {
|
||||
await runHookIfPresent(ctx, Hook.ON_BUILD_ERROR);
|
||||
});
|
||||
throw err;
|
||||
} finally {
|
||||
await ctx.runBuildPhase(BuildPhase.ON_BUILD_COMPLETE_HOOK, async () => {
|
||||
await runHookIfPresent(ctx, Hook.ON_BUILD_COMPLETE, {
|
||||
extraEnvs: {
|
||||
EAS_BUILD_STATUS: buildSuccess ? 'finished' : 'errored',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
if (ctx.job.platform === Platform.IOS) {
|
||||
await findAndUploadXcodeBuildLogsAsync(ctx as BuildContext<Ios.Job>, {
|
||||
logger: ctx.logger,
|
||||
});
|
||||
}
|
||||
|
||||
await ctx.runBuildPhase(BuildPhase.UPLOAD_BUILD_ARTIFACTS, async () => {
|
||||
await maybeFindAndUploadBuildArtifacts(ctx, {
|
||||
logger: ctx.logger,
|
||||
});
|
||||
});
|
||||
}
|
||||
} catch (err: any) {
|
||||
err.artifacts = ctx.artifacts;
|
||||
throw err;
|
||||
}
|
||||
|
||||
return ctx.artifacts;
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
import { BuildJob, BuildPhase, BuildTrigger, Ios, Platform } from '@expo/eas-build-job';
|
||||
import { BuildConfigParser, BuildStepGlobalContext, StepsConfigParser, errors } from '@expo/steps';
|
||||
import assert from 'assert';
|
||||
import fs from 'fs/promises';
|
||||
import nullthrows from 'nullthrows';
|
||||
import path from 'path';
|
||||
|
||||
import { resolveEnvFromBuildProfileAsync } from '../common/easBuildInternal';
|
||||
import { prepareProjectSourcesAsync } from '../common/projectSources';
|
||||
import { Artifacts, BuildContext } from '../context';
|
||||
import { CustomBuildContext } from '../customBuildContext';
|
||||
import { findAndUploadXcodeBuildLogsAsync } from '../ios/xcodeBuildLogs';
|
||||
import { getEasFunctionGroups } from '../steps/easFunctionGroups';
|
||||
import { getEasFunctions } from '../steps/easFunctions';
|
||||
import { retryAsync } from '../utils/retry';
|
||||
|
||||
export async function runCustomBuildAsync(ctx: BuildContext<BuildJob>): Promise<Artifacts> {
|
||||
const customBuildCtx = new CustomBuildContext(ctx);
|
||||
|
||||
await ctx.runBuildPhase(BuildPhase.PREPARE_PROJECT, async () => {
|
||||
await retryAsync(
|
||||
async () => {
|
||||
await fs.rm(customBuildCtx.projectSourceDirectory, { recursive: true, force: true });
|
||||
await fs.mkdir(customBuildCtx.projectSourceDirectory, { recursive: true });
|
||||
|
||||
await prepareProjectSourcesAsync(ctx, customBuildCtx.projectSourceDirectory);
|
||||
},
|
||||
{
|
||||
retryOptions: {
|
||||
retries: 3,
|
||||
retryIntervalMs: 1_000,
|
||||
},
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
if (ctx.job.triggeredBy === BuildTrigger.GIT_BASED_INTEGRATION) {
|
||||
// We need to setup envs from eas.json
|
||||
const env = await resolveEnvFromBuildProfileAsync(ctx, {
|
||||
cwd: path.join(customBuildCtx.projectSourceDirectory, ctx.job.projectRootDirectory ?? '.'),
|
||||
});
|
||||
ctx.updateEnv(env);
|
||||
customBuildCtx.updateEnv(ctx.env);
|
||||
}
|
||||
|
||||
assert(
|
||||
'steps' in ctx.job || 'customBuildConfig' in ctx.job,
|
||||
'Steps or custom build config path are required in custom jobs'
|
||||
);
|
||||
|
||||
const globalContext = new BuildStepGlobalContext(customBuildCtx, false);
|
||||
const easFunctions = getEasFunctions(customBuildCtx);
|
||||
const easFunctionGroups = getEasFunctionGroups(customBuildCtx);
|
||||
const parser = ctx.job.steps
|
||||
? new StepsConfigParser(globalContext, {
|
||||
externalFunctions: easFunctions,
|
||||
externalFunctionGroups: easFunctionGroups,
|
||||
steps: ctx.job.steps,
|
||||
})
|
||||
: new BuildConfigParser(globalContext, {
|
||||
externalFunctions: easFunctions,
|
||||
externalFunctionGroups: easFunctionGroups,
|
||||
configPath: path.join(
|
||||
ctx.getReactNativeProjectDirectory(customBuildCtx.projectSourceDirectory),
|
||||
nullthrows(
|
||||
ctx.job.customBuildConfig?.path,
|
||||
'Steps or custom build config path are required in custom jobs'
|
||||
)
|
||||
),
|
||||
});
|
||||
const workflow = await ctx.runBuildPhase(BuildPhase.PARSE_CUSTOM_WORKFLOW_CONFIG, async () => {
|
||||
try {
|
||||
return await parser.parseAsync();
|
||||
} catch (parseError: any) {
|
||||
ctx.logger.error('Failed to parse the custom build config file.');
|
||||
if (parseError instanceof errors.BuildWorkflowError) {
|
||||
for (const err of parseError.errors) {
|
||||
ctx.logger.error({ err });
|
||||
}
|
||||
}
|
||||
throw parseError;
|
||||
}
|
||||
});
|
||||
try {
|
||||
try {
|
||||
await workflow.executeAsync();
|
||||
} finally {
|
||||
if (!ctx.artifacts.XCODE_BUILD_LOGS && ctx.job.platform === Platform.IOS) {
|
||||
try {
|
||||
await findAndUploadXcodeBuildLogsAsync(ctx as BuildContext<Ios.Job>, {
|
||||
logger: ctx.logger,
|
||||
});
|
||||
} catch {
|
||||
// do nothing, it's a non-breaking error.
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err: any) {
|
||||
err.artifacts = ctx.artifacts;
|
||||
throw err;
|
||||
}
|
||||
|
||||
return ctx.artifacts;
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
import androidBuilder from './android';
|
||||
import iosBuilder from './ios';
|
||||
|
||||
export { androidBuilder, iosBuilder };
|
||||
@@ -0,0 +1,328 @@
|
||||
import { IOSConfig } from '@expo/config-plugins';
|
||||
import { BuildMode, BuildPhase, Ios, ManagedArtifactType, Workflow } from '@expo/eas-build-job';
|
||||
import plist from '@expo/plist';
|
||||
import fs from 'fs-extra';
|
||||
import nullthrows from 'nullthrows';
|
||||
|
||||
import { runBuilderWithHooksAsync } from './common';
|
||||
import { runCustomBuildAsync } from './custom';
|
||||
import { eagerBundleAsync, shouldUseEagerBundle } from '../common/eagerBundle';
|
||||
import { prebuildAsync } from '../common/prebuild';
|
||||
import { setupAsync } from '../common/setup';
|
||||
import { Artifacts, BuildContext } from '../context';
|
||||
import { configureXcodeProject } from '../ios/configure';
|
||||
import CredentialsManager from '../ios/credentials/manager';
|
||||
import { runFastlaneGym, runFastlaneResign } from '../ios/fastlane';
|
||||
import { installPods } from '../ios/pod';
|
||||
import { downloadApplicationArchiveAsync } from '../ios/resign';
|
||||
import { resolveArtifactPath, resolveBuildConfiguration, resolveScheme } from '../ios/resolve';
|
||||
import { cacheStatsAsync, restoreCcacheAsync } from '../steps/functions/restoreBuildCache';
|
||||
import { saveCcacheAsync } from '../steps/functions/saveBuildCache';
|
||||
import { uploadApplicationArchive } from '../utils/artifacts';
|
||||
import {
|
||||
configureExpoUpdatesIfInstalledAsync,
|
||||
resolveRuntimeVersionForExpoUpdatesIfConfiguredAsync,
|
||||
} from '../utils/expoUpdates';
|
||||
import { Hook, runHookIfPresent } from '../utils/hooks';
|
||||
import { prepareExecutableAsync } from '../utils/prepareBuildExecutable';
|
||||
import { getParentAndDescendantProcessPidsAsync } from '../utils/processes';
|
||||
|
||||
const INSTALL_PODS_WARN_TIMEOUT_MS = 15 * 60 * 1000; // 15 minutes
|
||||
const INSTALL_PODS_KILL_TIMEOUT_MS = 30 * 60 * 1000; // 30 minutes
|
||||
|
||||
class InstallPodsTimeoutError extends Error {}
|
||||
|
||||
export default async function iosBuilder(ctx: BuildContext<Ios.Job>): Promise<Artifacts> {
|
||||
if (ctx.job.mode === BuildMode.BUILD) {
|
||||
await prepareExecutableAsync(ctx);
|
||||
return await runBuilderWithHooksAsync(ctx, buildAsync);
|
||||
} else if (ctx.job.mode === BuildMode.RESIGN) {
|
||||
return await resignAsync(ctx);
|
||||
} else if (ctx.job.mode === BuildMode.CUSTOM || ctx.job.mode === BuildMode.REPACK) {
|
||||
return await runCustomBuildAsync(ctx);
|
||||
} else {
|
||||
throw new Error('Not implemented');
|
||||
}
|
||||
}
|
||||
|
||||
async function buildAsync(ctx: BuildContext<Ios.Job>): Promise<void> {
|
||||
await setupAsync(ctx);
|
||||
const hasNativeCode = ctx.job.type === Workflow.GENERIC;
|
||||
const evictUsedBefore = new Date();
|
||||
const credentialsManager = new CredentialsManager(ctx);
|
||||
const workingDirectory = ctx.getReactNativeProjectDirectory();
|
||||
try {
|
||||
const credentials = await ctx.runBuildPhase(BuildPhase.PREPARE_CREDENTIALS, async () => {
|
||||
return await credentialsManager.prepare();
|
||||
});
|
||||
|
||||
await ctx.runBuildPhase(BuildPhase.PREBUILD, async () => {
|
||||
if (hasNativeCode) {
|
||||
ctx.markBuildPhaseSkipped();
|
||||
ctx.logger.info(
|
||||
'Skipped running "expo prebuild" because the "ios" directory already exists. Learn more about the build process: https://docs.expo.dev/build-reference/ios-builds/'
|
||||
);
|
||||
return;
|
||||
}
|
||||
const extraEnvs: Record<string, string> = credentials?.teamId
|
||||
? { APPLE_TEAM_ID: credentials.teamId }
|
||||
: {};
|
||||
await prebuildAsync(ctx, {
|
||||
logger: ctx.logger,
|
||||
workingDir: ctx.getReactNativeProjectDirectory(),
|
||||
options: { extraEnvs },
|
||||
});
|
||||
});
|
||||
|
||||
await ctx.runBuildPhase(BuildPhase.RESTORE_CACHE, async () => {
|
||||
if (ctx.isLocal) {
|
||||
ctx.logger.info('Local builds do not support restoring cache');
|
||||
return;
|
||||
}
|
||||
await ctx.cacheManager?.restoreCache(ctx);
|
||||
await restoreCcacheAsync({
|
||||
logger: ctx.logger,
|
||||
workingDirectory,
|
||||
platform: ctx.job.platform,
|
||||
env: ctx.env,
|
||||
secrets: ctx.job.secrets,
|
||||
});
|
||||
});
|
||||
|
||||
await ctx.runBuildPhase(BuildPhase.INSTALL_PODS, async () => {
|
||||
await runInstallPodsAsync(ctx);
|
||||
});
|
||||
|
||||
await ctx.runBuildPhase(BuildPhase.POST_INSTALL_HOOK, async () => {
|
||||
await runHookIfPresent(ctx, Hook.POST_INSTALL);
|
||||
});
|
||||
|
||||
const resolvedExpoUpdatesRuntimeVersion = await ctx.runBuildPhase(
|
||||
BuildPhase.CALCULATE_EXPO_UPDATES_RUNTIME_VERSION,
|
||||
async () => {
|
||||
return await resolveRuntimeVersionForExpoUpdatesIfConfiguredAsync({
|
||||
cwd: ctx.getReactNativeProjectDirectory(),
|
||||
logger: ctx.logger,
|
||||
appConfig: ctx.appConfig,
|
||||
platform: ctx.job.platform,
|
||||
workflow: ctx.job.type,
|
||||
env: ctx.env,
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
const buildConfiguration = resolveBuildConfiguration(ctx);
|
||||
if (credentials) {
|
||||
await ctx.runBuildPhase(BuildPhase.CONFIGURE_XCODE_PROJECT, async () => {
|
||||
await configureXcodeProject(ctx, { credentials, buildConfiguration });
|
||||
});
|
||||
}
|
||||
|
||||
await ctx.runBuildPhase(BuildPhase.CONFIGURE_EXPO_UPDATES, async () => {
|
||||
await configureExpoUpdatesIfInstalledAsync(ctx, {
|
||||
resolvedRuntimeVersion: resolvedExpoUpdatesRuntimeVersion?.runtimeVersion ?? null,
|
||||
resolvedFingerprintSources: resolvedExpoUpdatesRuntimeVersion?.fingerprintSources ?? null,
|
||||
});
|
||||
});
|
||||
|
||||
if (!ctx.env.EAS_BUILD_DISABLE_BUNDLE_JAVASCRIPT_STEP && shouldUseEagerBundle(ctx.metadata)) {
|
||||
await ctx.runBuildPhase(BuildPhase.EAGER_BUNDLE, async () => {
|
||||
await eagerBundleAsync({
|
||||
platform: ctx.job.platform,
|
||||
workingDir: ctx.getReactNativeProjectDirectory(),
|
||||
logger: ctx.logger,
|
||||
env: {
|
||||
...ctx.env,
|
||||
...(resolvedExpoUpdatesRuntimeVersion?.runtimeVersion
|
||||
? {
|
||||
EXPO_UPDATES_FINGERPRINT_OVERRIDE:
|
||||
resolvedExpoUpdatesRuntimeVersion?.runtimeVersion,
|
||||
EXPO_UPDATES_WORKFLOW_OVERRIDE: ctx.job.type,
|
||||
}
|
||||
: null),
|
||||
},
|
||||
packageManager: ctx.packageManager,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
await ctx.runBuildPhase(BuildPhase.RUN_FASTLANE, async () => {
|
||||
const scheme = resolveScheme(ctx);
|
||||
const entitlements = await readEntitlementsAsync(ctx, { scheme, buildConfiguration });
|
||||
await runFastlaneGym(ctx, {
|
||||
credentials,
|
||||
scheme,
|
||||
buildConfiguration,
|
||||
entitlements,
|
||||
...(resolvedExpoUpdatesRuntimeVersion?.runtimeVersion
|
||||
? {
|
||||
extraEnv: {
|
||||
EXPO_UPDATES_FINGERPRINT_OVERRIDE:
|
||||
resolvedExpoUpdatesRuntimeVersion?.runtimeVersion,
|
||||
EXPO_UPDATES_WORKFLOW_OVERRIDE: ctx.job.type,
|
||||
},
|
||||
}
|
||||
: null),
|
||||
});
|
||||
});
|
||||
} finally {
|
||||
await ctx.runBuildPhase(BuildPhase.CLEAN_UP_CREDENTIALS, async () => {
|
||||
await credentialsManager.cleanUp();
|
||||
});
|
||||
}
|
||||
|
||||
await ctx.runBuildPhase(BuildPhase.PRE_UPLOAD_ARTIFACTS_HOOK, async () => {
|
||||
await runHookIfPresent(ctx, Hook.PRE_UPLOAD_ARTIFACTS);
|
||||
});
|
||||
|
||||
await ctx.runBuildPhase(BuildPhase.UPLOAD_APPLICATION_ARCHIVE, async () => {
|
||||
await uploadApplicationArchive(ctx, {
|
||||
patternOrPath: resolveArtifactPath(ctx),
|
||||
rootDir: ctx.getReactNativeProjectDirectory(),
|
||||
logger: ctx.logger,
|
||||
});
|
||||
});
|
||||
|
||||
await ctx.runBuildPhase(BuildPhase.SAVE_CACHE, async () => {
|
||||
if (ctx.isLocal) {
|
||||
ctx.logger.info('Local builds do not support saving cache.');
|
||||
return;
|
||||
}
|
||||
await ctx.cacheManager?.saveCache(ctx);
|
||||
await saveCcacheAsync({
|
||||
logger: ctx.logger,
|
||||
workingDirectory,
|
||||
platform: ctx.job.platform,
|
||||
evictUsedBefore,
|
||||
env: ctx.env,
|
||||
secrets: ctx.job.secrets,
|
||||
});
|
||||
});
|
||||
|
||||
await ctx.runBuildPhase(BuildPhase.CACHE_STATS, async () => {
|
||||
await cacheStatsAsync({
|
||||
logger: ctx.logger,
|
||||
env: ctx.env,
|
||||
secrets: ctx.job.secrets,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function readEntitlementsAsync(
|
||||
ctx: BuildContext<Ios.Job>,
|
||||
{ scheme, buildConfiguration }: { scheme: string; buildConfiguration: string }
|
||||
): Promise<object | null> {
|
||||
try {
|
||||
const applicationTargetName =
|
||||
await IOSConfig.BuildScheme.getApplicationTargetNameForSchemeAsync(
|
||||
ctx.getReactNativeProjectDirectory(),
|
||||
scheme
|
||||
);
|
||||
const entitlementsPath = IOSConfig.Entitlements.getEntitlementsPath(
|
||||
ctx.getReactNativeProjectDirectory(),
|
||||
{
|
||||
buildConfiguration,
|
||||
targetName: applicationTargetName,
|
||||
}
|
||||
);
|
||||
if (!entitlementsPath) {
|
||||
return null;
|
||||
}
|
||||
const entitlementsRaw = await fs.readFile(entitlementsPath, 'utf8');
|
||||
return plist.parse(entitlementsRaw);
|
||||
} catch (err) {
|
||||
ctx.logger.warn({ err }, 'Failed to read entitlements');
|
||||
ctx.markBuildPhaseHasWarnings();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function resignAsync(ctx: BuildContext<Ios.Job>): Promise<Artifacts> {
|
||||
const applicationArchivePath = await ctx.runBuildPhase(
|
||||
BuildPhase.DOWNLOAD_APPLICATION_ARCHIVE,
|
||||
async () => {
|
||||
return await downloadApplicationArchiveAsync(ctx);
|
||||
}
|
||||
);
|
||||
|
||||
const credentialsManager = new CredentialsManager(ctx);
|
||||
try {
|
||||
const credentials = await ctx.runBuildPhase(BuildPhase.PREPARE_CREDENTIALS, async () => {
|
||||
return await credentialsManager.prepare();
|
||||
});
|
||||
|
||||
await ctx.runBuildPhase(BuildPhase.RUN_FASTLANE, async () => {
|
||||
await runFastlaneResign(ctx, {
|
||||
credentials: nullthrows(credentials),
|
||||
ipaPath: applicationArchivePath,
|
||||
});
|
||||
});
|
||||
} finally {
|
||||
await ctx.runBuildPhase(BuildPhase.CLEAN_UP_CREDENTIALS, async () => {
|
||||
await credentialsManager.cleanUp();
|
||||
});
|
||||
}
|
||||
|
||||
await ctx.runBuildPhase(BuildPhase.UPLOAD_APPLICATION_ARCHIVE, async () => {
|
||||
ctx.logger.info(`Application archive: ${applicationArchivePath}`);
|
||||
await ctx.uploadArtifact({
|
||||
artifact: {
|
||||
type: ManagedArtifactType.APPLICATION_ARCHIVE,
|
||||
paths: [applicationArchivePath],
|
||||
},
|
||||
logger: ctx.logger,
|
||||
});
|
||||
});
|
||||
|
||||
return ctx.artifacts;
|
||||
}
|
||||
|
||||
async function runInstallPodsAsync(ctx: BuildContext<Ios.Job>): Promise<void> {
|
||||
let warnTimeout: NodeJS.Timeout | undefined;
|
||||
let killTimeout: NodeJS.Timeout | undefined;
|
||||
let timedOutToKill: boolean = false;
|
||||
try {
|
||||
const installPodsSpawnPromise = (
|
||||
await installPods(ctx, {
|
||||
infoCallbackFn: () => {
|
||||
warnTimeout?.refresh();
|
||||
killTimeout?.refresh();
|
||||
},
|
||||
})
|
||||
).spawnPromise;
|
||||
warnTimeout = setTimeout(() => {
|
||||
ctx.logger.warn(
|
||||
'"Install pods" phase takes longer then expected and it did not produce any logs in the past 15 minutes'
|
||||
);
|
||||
}, INSTALL_PODS_WARN_TIMEOUT_MS);
|
||||
|
||||
killTimeout = setTimeout(async () => {
|
||||
timedOutToKill = true;
|
||||
ctx.logger.error(
|
||||
'"Install pods" phase takes a very long time and it did not produce any logs in the past 30 minutes. Most likely an unexpected error happened which caused the process to hang and it will be terminated'
|
||||
);
|
||||
const ppid = nullthrows(installPodsSpawnPromise.child.pid);
|
||||
const pids = await getParentAndDescendantProcessPidsAsync(ppid);
|
||||
pids.forEach(pid => {
|
||||
process.kill(pid);
|
||||
});
|
||||
ctx.reportError?.('"Install pods" phase takes a very long time', undefined, {
|
||||
extras: { buildId: ctx.env.EAS_BUILD_ID },
|
||||
});
|
||||
}, INSTALL_PODS_KILL_TIMEOUT_MS);
|
||||
|
||||
await installPodsSpawnPromise;
|
||||
} catch (err: any) {
|
||||
if (timedOutToKill) {
|
||||
throw new InstallPodsTimeoutError('"Install pods" phase was inactive for over 30 minutes');
|
||||
}
|
||||
throw err;
|
||||
} finally {
|
||||
if (warnTimeout) {
|
||||
clearTimeout(warnTimeout);
|
||||
}
|
||||
if (killTimeout) {
|
||||
clearTimeout(killTimeout);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,575 @@
|
||||
import {
|
||||
ArchiveSourceType,
|
||||
BuildMode,
|
||||
BuildTrigger,
|
||||
Job,
|
||||
Platform,
|
||||
Workflow,
|
||||
} from '@expo/eas-build-job';
|
||||
import { randomBytes, randomUUID } from 'crypto';
|
||||
import { vol } from 'memfs';
|
||||
import fetch, { Response } from 'node-fetch';
|
||||
import { setTimeout } from 'timers/promises';
|
||||
|
||||
import { createMockLogger } from '../../__tests__/utils/logger';
|
||||
import { BuildContext } from '../../context';
|
||||
import { shallowCloneRepositoryAsync } from '../git';
|
||||
import { prepareProjectSourcesAsync } from '../projectSources';
|
||||
|
||||
jest.mock('@expo/turtle-spawn');
|
||||
jest.mock('node-fetch');
|
||||
jest.mock('../git');
|
||||
jest.mock('@expo/downloader');
|
||||
jest.mock('@urql/core');
|
||||
|
||||
describe('projectSources', () => {
|
||||
it('should use the refreshed repository URL', async () => {
|
||||
const robotAccessToken = randomUUID();
|
||||
const buildId = randomUUID();
|
||||
await vol.promises.mkdir('/workingdir/environment-secrets/', { recursive: true });
|
||||
|
||||
const gitCommitHash = randomBytes(20).toString('hex');
|
||||
|
||||
const ctx = new BuildContext(
|
||||
{
|
||||
triggeredBy: BuildTrigger.GIT_BASED_INTEGRATION,
|
||||
type: Workflow.MANAGED,
|
||||
mode: BuildMode.BUILD,
|
||||
initiatingUserId: randomUUID(),
|
||||
appId: randomUUID(),
|
||||
projectArchive: {
|
||||
type: ArchiveSourceType.GIT,
|
||||
repositoryUrl: 'https://x-access-token:[email protected]/expo/eas-cli.git',
|
||||
gitRef: 'refs/heads/main',
|
||||
gitCommitHash,
|
||||
},
|
||||
platform: Platform.IOS,
|
||||
secrets: {
|
||||
robotAccessToken,
|
||||
environmentSecrets: [],
|
||||
},
|
||||
} as Job,
|
||||
{
|
||||
env: {
|
||||
__API_SERVER_URL: 'https://api.expo.dev',
|
||||
EXPO_TOKEN: robotAccessToken,
|
||||
EAS_BUILD_ID: buildId,
|
||||
EAS_BUILD_RUNNER: 'eas-build',
|
||||
},
|
||||
workingdir: '/workingdir',
|
||||
logger: createMockLogger(),
|
||||
logBuffer: { getLogs: () => [], getPhaseLogs: () => [] },
|
||||
uploadArtifact: jest.fn(),
|
||||
}
|
||||
);
|
||||
const fetchMock = jest.mocked(fetch);
|
||||
fetchMock.mockImplementation(
|
||||
async () =>
|
||||
({
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
data: {
|
||||
gitRef: 'refs/heads/main',
|
||||
gitCommitHash,
|
||||
repositoryUrl: 'https://x-access-token:[email protected]/expo/eas-cli.git',
|
||||
type: ArchiveSourceType.GIT,
|
||||
},
|
||||
}),
|
||||
}) as Response
|
||||
);
|
||||
|
||||
await prepareProjectSourcesAsync(ctx, ctx.buildDirectory);
|
||||
expect(shallowCloneRepositoryAsync).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
archiveSource: {
|
||||
...ctx.job.projectArchive,
|
||||
repositoryUrl: 'https://x-access-token:[email protected]/expo/eas-cli.git',
|
||||
},
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should fallback to the original repository URL if the refresh fails', async () => {
|
||||
const robotAccessToken = randomUUID();
|
||||
const buildId = randomUUID();
|
||||
await vol.promises.mkdir('/workingdir/environment-secrets/', { recursive: true });
|
||||
|
||||
const ctx = new BuildContext(
|
||||
{
|
||||
triggeredBy: BuildTrigger.GIT_BASED_INTEGRATION,
|
||||
type: Workflow.MANAGED,
|
||||
mode: BuildMode.BUILD,
|
||||
initiatingUserId: randomUUID(),
|
||||
appId: randomUUID(),
|
||||
projectArchive: {
|
||||
type: ArchiveSourceType.GIT,
|
||||
repositoryUrl: 'https://x-access-token:[email protected]/expo/eas-cli.git',
|
||||
gitRef: 'refs/heads/main',
|
||||
gitCommitHash: randomBytes(20).toString('hex'),
|
||||
},
|
||||
platform: Platform.IOS,
|
||||
secrets: {
|
||||
robotAccessToken,
|
||||
environmentSecrets: [],
|
||||
},
|
||||
} as Job,
|
||||
{
|
||||
env: {
|
||||
__API_SERVER_URL: 'https://api.expo.dev',
|
||||
EXPO_TOKEN: robotAccessToken,
|
||||
EAS_BUILD_ID: buildId,
|
||||
EAS_BUILD_RUNNER: 'eas-build',
|
||||
},
|
||||
workingdir: '/workingdir',
|
||||
logger: createMockLogger(),
|
||||
logBuffer: { getLogs: () => [], getPhaseLogs: () => [] },
|
||||
uploadArtifact: jest.fn(),
|
||||
}
|
||||
);
|
||||
const fetchMock = jest.mocked(fetch);
|
||||
fetchMock.mockImplementation(
|
||||
async () =>
|
||||
({
|
||||
ok: false,
|
||||
text: async () => 'Failed to generate repository URL',
|
||||
}) as Response
|
||||
);
|
||||
|
||||
await prepareProjectSourcesAsync(ctx, ctx.buildDirectory);
|
||||
expect(shallowCloneRepositoryAsync).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({
|
||||
archiveSource: {
|
||||
...ctx.job.projectArchive,
|
||||
repositoryUrl: 'https://x-access-token:[email protected]/expo/eas-cli.git',
|
||||
},
|
||||
})
|
||||
);
|
||||
fetchMock.mockImplementation(
|
||||
async () =>
|
||||
({
|
||||
ok: false,
|
||||
json: async () => ({
|
||||
// repositoryUrl is the right key
|
||||
repository_url: 'https://x-access-token:[email protected]/expo/eas-cli.git',
|
||||
}),
|
||||
}) as Response
|
||||
);
|
||||
|
||||
await prepareProjectSourcesAsync(ctx, ctx.buildDirectory);
|
||||
expect(shallowCloneRepositoryAsync).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({
|
||||
archiveSource: {
|
||||
...ctx.job.projectArchive,
|
||||
repositoryUrl: 'https://x-access-token:[email protected]/expo/eas-cli.git',
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
fetchMock.mockImplementation(
|
||||
async () =>
|
||||
({
|
||||
ok: false,
|
||||
json: () => Promise.reject(new Error('Failed to generate repository URL')),
|
||||
}) as Response
|
||||
);
|
||||
|
||||
await prepareProjectSourcesAsync(ctx, ctx.buildDirectory);
|
||||
expect(shallowCloneRepositoryAsync).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({
|
||||
archiveSource: {
|
||||
...ctx.job.projectArchive,
|
||||
repositoryUrl: 'https://x-access-token:[email protected]/expo/eas-cli.git',
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
expect(shallowCloneRepositoryAsync).toHaveBeenCalledTimes(3);
|
||||
}, 15_000);
|
||||
|
||||
it('should retry fetching the repository URL', async () => {
|
||||
const robotAccessToken = randomUUID();
|
||||
const buildId = randomUUID();
|
||||
await vol.promises.mkdir('/workingdir/environment-secrets/', { recursive: true });
|
||||
|
||||
const gitCommitHash = randomBytes(20).toString('hex');
|
||||
|
||||
const ctx = new BuildContext(
|
||||
{
|
||||
triggeredBy: BuildTrigger.GIT_BASED_INTEGRATION,
|
||||
type: Workflow.MANAGED,
|
||||
mode: BuildMode.BUILD,
|
||||
initiatingUserId: randomUUID(),
|
||||
appId: randomUUID(),
|
||||
projectArchive: {
|
||||
type: ArchiveSourceType.GIT,
|
||||
repositoryUrl: 'https://x-access-token:[email protected]/expo/eas-cli.git',
|
||||
gitRef: 'refs/heads/main',
|
||||
gitCommitHash,
|
||||
},
|
||||
platform: Platform.IOS,
|
||||
secrets: {
|
||||
robotAccessToken,
|
||||
environmentSecrets: [],
|
||||
},
|
||||
} as Job,
|
||||
{
|
||||
env: {
|
||||
__API_SERVER_URL: 'https://api.expo.dev',
|
||||
EXPO_TOKEN: robotAccessToken,
|
||||
EAS_BUILD_ID: buildId,
|
||||
EAS_BUILD_RUNNER: 'eas-build',
|
||||
},
|
||||
workingdir: '/workingdir',
|
||||
logger: createMockLogger(),
|
||||
logBuffer: { getLogs: () => [], getPhaseLogs: () => [] },
|
||||
uploadArtifact: jest.fn(),
|
||||
}
|
||||
);
|
||||
const fetchMock = jest.mocked(fetch);
|
||||
fetchMock.mockImplementationOnce(
|
||||
async () =>
|
||||
({
|
||||
ok: false,
|
||||
text: async () => 'Failed to generate repository URL',
|
||||
}) as Response
|
||||
);
|
||||
fetchMock.mockImplementationOnce(
|
||||
async () =>
|
||||
({
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
data: {
|
||||
repositoryUrl: 'https://x-access-token:[email protected]/expo/eas-cli.git',
|
||||
gitRef: 'refs/heads/main',
|
||||
gitCommitHash,
|
||||
type: ArchiveSourceType.GIT,
|
||||
},
|
||||
}),
|
||||
}) as Response
|
||||
);
|
||||
|
||||
await prepareProjectSourcesAsync(ctx, ctx.buildDirectory);
|
||||
expect(shallowCloneRepositoryAsync).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({
|
||||
archiveSource: {
|
||||
...ctx.job.projectArchive,
|
||||
repositoryUrl: 'https://x-access-token:[email protected]/expo/eas-cli.git',
|
||||
},
|
||||
})
|
||||
);
|
||||
}, 15_000);
|
||||
|
||||
it(`should fallback to the original repository URL if we're missing some config`, async () => {
|
||||
const robotAccessToken = randomUUID();
|
||||
await vol.promises.mkdir('/workingdir/environment-secrets/', { recursive: true });
|
||||
const logger = createMockLogger();
|
||||
|
||||
const ctx = new BuildContext(
|
||||
{
|
||||
triggeredBy: BuildTrigger.GIT_BASED_INTEGRATION,
|
||||
type: Workflow.MANAGED,
|
||||
mode: BuildMode.BUILD,
|
||||
initiatingUserId: randomUUID(),
|
||||
appId: randomUUID(),
|
||||
projectArchive: {
|
||||
type: ArchiveSourceType.GIT,
|
||||
repositoryUrl: 'https://x-access-token:[email protected]/expo/eas-cli.git',
|
||||
gitRef: 'refs/heads/main',
|
||||
gitCommitHash: randomBytes(20).toString('hex'),
|
||||
},
|
||||
platform: Platform.IOS,
|
||||
secrets: {
|
||||
robotAccessToken,
|
||||
environmentSecrets: [],
|
||||
},
|
||||
} as Job,
|
||||
{
|
||||
env: {
|
||||
__API_SERVER_URL: 'https://api.expo.dev',
|
||||
EXPO_TOKEN: robotAccessToken,
|
||||
EAS_BUILD_RUNNER: 'eas-build',
|
||||
// EAS_BUILD_ID: buildId,
|
||||
},
|
||||
workingdir: '/workingdir',
|
||||
logger,
|
||||
logBuffer: { getLogs: () => [], getPhaseLogs: () => [] },
|
||||
uploadArtifact: jest.fn(),
|
||||
}
|
||||
);
|
||||
|
||||
await prepareProjectSourcesAsync(ctx, ctx.buildDirectory);
|
||||
|
||||
expect(logger.error).toHaveBeenCalledWith(
|
||||
{ err: expect.any(Error) },
|
||||
'Failed to refresh project archive, falling back to the original one'
|
||||
);
|
||||
});
|
||||
|
||||
describe('uploadProjectMetadataAsFireAndForget', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should upload project metadata for build job with URL archive type', async () => {
|
||||
const robotAccessToken = randomUUID();
|
||||
const buildId = randomUUID();
|
||||
const bucketKey = `test-bucket-key-${randomUUID()}`;
|
||||
await vol.promises.mkdir('/workingdir/build', { recursive: true });
|
||||
await vol.promises.writeFile('/workingdir/build/package.json', '{}');
|
||||
await vol.promises.writeFile('/workingdir/build/README.md', 'Hello, world!');
|
||||
|
||||
const mockGraphqlMutation = jest.fn();
|
||||
mockGraphqlMutation
|
||||
.mockReturnValueOnce({
|
||||
toPromise: () =>
|
||||
Promise.resolve({
|
||||
data: {
|
||||
uploadSession: {
|
||||
createUploadSession: {
|
||||
url: 'https://storage.example.com/upload',
|
||||
bucketKey,
|
||||
headers: { 'x-custom-header': 'value' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
})
|
||||
.mockReturnValueOnce({
|
||||
toPromise: () =>
|
||||
Promise.resolve({
|
||||
data: {
|
||||
build: {
|
||||
updateBuildMetadata: {
|
||||
id: buildId,
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
const ctx = new BuildContext(
|
||||
{
|
||||
triggeredBy: BuildTrigger.EAS_CLI,
|
||||
type: Workflow.MANAGED,
|
||||
mode: BuildMode.BUILD,
|
||||
initiatingUserId: randomUUID(),
|
||||
appId: randomUUID(),
|
||||
projectArchive: {
|
||||
type: ArchiveSourceType.URL,
|
||||
url: 'https://example.com/project.tar.gz',
|
||||
},
|
||||
platform: Platform.IOS,
|
||||
secrets: {
|
||||
robotAccessToken,
|
||||
environmentSecrets: [],
|
||||
},
|
||||
} as Job,
|
||||
{
|
||||
env: {
|
||||
__API_SERVER_URL: 'https://api.expo.dev',
|
||||
EXPO_TOKEN: robotAccessToken,
|
||||
EAS_BUILD_ID: buildId,
|
||||
EAS_BUILD_RUNNER: 'eas-build',
|
||||
},
|
||||
workingdir: '/workingdir',
|
||||
logger: createMockLogger(),
|
||||
logBuffer: { getLogs: () => [], getPhaseLogs: () => [] },
|
||||
uploadArtifact: jest.fn(),
|
||||
}
|
||||
);
|
||||
|
||||
ctx.graphqlClient.mutation = mockGraphqlMutation as any;
|
||||
|
||||
const fetchMock = jest.mocked(fetch);
|
||||
fetchMock.mockImplementation(async () => ({ ok: true }) as Response);
|
||||
|
||||
// Call prepareProjectSourcesAsync and don't await metadata upload
|
||||
await prepareProjectSourcesAsync(ctx, ctx.buildDirectory);
|
||||
|
||||
// Wait for the fire-and-forget async operation to complete
|
||||
await setTimeout(1000);
|
||||
|
||||
// Verify the upload session was created
|
||||
expect(mockGraphqlMutation).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
expect.anything(),
|
||||
expect.objectContaining({})
|
||||
);
|
||||
|
||||
// Verify metadata was uploaded to storage
|
||||
expect(fetchMock).toHaveBeenCalledWith(
|
||||
'https://storage.example.com/upload',
|
||||
expect.objectContaining({
|
||||
method: 'PUT',
|
||||
headers: { 'x-custom-header': 'value' },
|
||||
body: '{"archiveContent":["project/README.md","project/package.json"]}',
|
||||
})
|
||||
);
|
||||
|
||||
// Verify build metadata was updated
|
||||
expect(mockGraphqlMutation).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
expect.anything(),
|
||||
expect.objectContaining({
|
||||
buildId,
|
||||
projectMetadataFile: {
|
||||
type: 'GCS',
|
||||
bucketKey,
|
||||
},
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should not await the metadata upload (fire-and-forget)', async () => {
|
||||
const robotAccessToken = randomUUID();
|
||||
const buildId = randomUUID();
|
||||
await vol.promises.mkdir('/workingdir/build', { recursive: true });
|
||||
await vol.promises.writeFile('/workingdir/build/app.json', '{}');
|
||||
|
||||
let uploadCompleted = false;
|
||||
const mockGraphqlMutation = jest.fn();
|
||||
mockGraphqlMutation
|
||||
.mockReturnValueOnce({
|
||||
toPromise: async () => {
|
||||
// Simulate a slow upload
|
||||
await setTimeout(100);
|
||||
uploadCompleted = true;
|
||||
return {
|
||||
data: {
|
||||
uploadSession: {
|
||||
createUploadSession: {
|
||||
url: 'https://storage.example.com/upload',
|
||||
bucketKey: 'test-key',
|
||||
headers: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
})
|
||||
.mockReturnValueOnce({
|
||||
toPromise: () =>
|
||||
Promise.resolve({
|
||||
data: {
|
||||
build: {
|
||||
updateBuildMetadata: {
|
||||
id: buildId,
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
const ctx = new BuildContext(
|
||||
{
|
||||
triggeredBy: BuildTrigger.EAS_CLI,
|
||||
type: Workflow.MANAGED,
|
||||
mode: BuildMode.BUILD,
|
||||
initiatingUserId: randomUUID(),
|
||||
appId: randomUUID(),
|
||||
projectArchive: {
|
||||
type: ArchiveSourceType.URL,
|
||||
url: 'https://example.com/project.tar.gz',
|
||||
},
|
||||
platform: Platform.IOS,
|
||||
secrets: {
|
||||
robotAccessToken,
|
||||
environmentSecrets: [],
|
||||
},
|
||||
} as Job,
|
||||
{
|
||||
env: {
|
||||
__API_SERVER_URL: 'https://api.expo.dev',
|
||||
EXPO_TOKEN: robotAccessToken,
|
||||
EAS_BUILD_ID: buildId,
|
||||
EAS_BUILD_RUNNER: 'eas-build',
|
||||
},
|
||||
workingdir: '/workingdir',
|
||||
logger: createMockLogger(),
|
||||
logBuffer: { getLogs: () => [], getPhaseLogs: () => [] },
|
||||
uploadArtifact: jest.fn(),
|
||||
}
|
||||
);
|
||||
|
||||
ctx.graphqlClient.mutation = mockGraphqlMutation as any;
|
||||
|
||||
const fetchMock = jest.mocked(fetch);
|
||||
fetchMock.mockResolvedValue({ ok: true } as Response);
|
||||
|
||||
const startTime = Date.now();
|
||||
await prepareProjectSourcesAsync(ctx, ctx.buildDirectory);
|
||||
const endTime = Date.now();
|
||||
|
||||
// prepareProjectSourcesAsync should complete quickly without waiting for upload
|
||||
expect(endTime - startTime).toBeLessThan(50);
|
||||
// Upload should not be completed yet
|
||||
expect(uploadCompleted).toBe(false);
|
||||
|
||||
// Wait for the fire-and-forget operation to complete
|
||||
await setTimeout(150);
|
||||
|
||||
// Now the upload should be completed
|
||||
expect(uploadCompleted).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle upload errors gracefully without failing the build', async () => {
|
||||
const robotAccessToken = randomUUID();
|
||||
const buildId = randomUUID();
|
||||
await vol.promises.mkdir('/workingdir/build', { recursive: true });
|
||||
await vol.promises.writeFile('/workingdir/build/index.js', 'console.log("test");');
|
||||
|
||||
const mockGraphqlMutation = jest.fn();
|
||||
mockGraphqlMutation.mockReturnValue({
|
||||
toPromise: () => Promise.reject(new Error('Upload failed')),
|
||||
});
|
||||
|
||||
const logger = createMockLogger();
|
||||
const ctx = new BuildContext(
|
||||
{
|
||||
triggeredBy: BuildTrigger.GIT_BASED_INTEGRATION,
|
||||
type: Workflow.MANAGED,
|
||||
mode: BuildMode.BUILD,
|
||||
initiatingUserId: randomUUID(),
|
||||
appId: randomUUID(),
|
||||
projectArchive: {
|
||||
type: ArchiveSourceType.URL,
|
||||
url: 'https://example.com/project.tar.gz',
|
||||
},
|
||||
platform: Platform.IOS,
|
||||
secrets: {
|
||||
robotAccessToken,
|
||||
environmentSecrets: [],
|
||||
},
|
||||
} as Job,
|
||||
{
|
||||
env: {
|
||||
__API_SERVER_URL: 'https://api.expo.dev',
|
||||
EXPO_TOKEN: robotAccessToken,
|
||||
EAS_BUILD_ID: buildId,
|
||||
EAS_BUILD_RUNNER: 'eas-build',
|
||||
},
|
||||
workingdir: '/workingdir',
|
||||
logger,
|
||||
logBuffer: { getLogs: () => [], getPhaseLogs: () => [] },
|
||||
uploadArtifact: jest.fn(),
|
||||
}
|
||||
);
|
||||
|
||||
ctx.graphqlClient.mutation = mockGraphqlMutation as any;
|
||||
|
||||
const fetchMock = jest.mocked(fetch);
|
||||
fetchMock.mockResolvedValue({ ok: true } as Response);
|
||||
|
||||
// Should not throw even though upload will fail
|
||||
await expect(prepareProjectSourcesAsync(ctx, ctx.buildDirectory)).resolves.not.toThrow();
|
||||
|
||||
// Wait for the fire-and-forget operation to complete
|
||||
await setTimeout(100);
|
||||
|
||||
// Verify that a warning was logged
|
||||
expect(logger.warn).toHaveBeenCalledWith(
|
||||
expect.stringContaining('Failed to upload project metadata')
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,38 @@
|
||||
import { Metadata, Platform } from '@expo/eas-build-job';
|
||||
import { bunyan } from '@expo/logger';
|
||||
import semver from 'semver';
|
||||
|
||||
import { PackageManager } from '../utils/packageManager';
|
||||
import { runExpoCliCommand } from '../utils/project';
|
||||
|
||||
export async function eagerBundleAsync({
|
||||
platform,
|
||||
workingDir,
|
||||
logger,
|
||||
env,
|
||||
packageManager,
|
||||
}: {
|
||||
platform: Platform;
|
||||
workingDir: string;
|
||||
logger: bunyan;
|
||||
env: Record<string, string | undefined>;
|
||||
packageManager: PackageManager;
|
||||
}): Promise<void> {
|
||||
await runExpoCliCommand({
|
||||
args: ['export:embed', '--eager', '--platform', platform, '--dev', 'false'],
|
||||
options: {
|
||||
cwd: workingDir,
|
||||
logger,
|
||||
env,
|
||||
},
|
||||
packageManager,
|
||||
});
|
||||
}
|
||||
|
||||
export function shouldUseEagerBundle(metadata: Metadata | null | undefined): boolean {
|
||||
return Boolean(
|
||||
!metadata?.developmentClient &&
|
||||
metadata?.sdkVersion &&
|
||||
semver.satisfies(metadata?.sdkVersion, '>=52')
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
import {
|
||||
BuildJob,
|
||||
EasCliNpmTags,
|
||||
Env,
|
||||
Metadata,
|
||||
sanitizeBuildJob,
|
||||
sanitizeMetadata,
|
||||
} from '@expo/eas-build-job';
|
||||
import { PipeMode, bunyan } from '@expo/logger';
|
||||
import { BuildStepEnv } from '@expo/steps';
|
||||
import spawn from '@expo/turtle-spawn';
|
||||
import assert from 'assert';
|
||||
import Joi from 'joi';
|
||||
import nullthrows from 'nullthrows';
|
||||
|
||||
import { BuildContext } from '../context';
|
||||
import { isAtLeastNpm7Async } from '../utils/packageManager';
|
||||
|
||||
const EasBuildInternalResultSchema = Joi.object<{ job: object; metadata: object }>({
|
||||
job: Joi.object().unknown(),
|
||||
metadata: Joi.object().unknown(),
|
||||
});
|
||||
|
||||
export async function runEasBuildInternalAsync<TJob extends BuildJob>({
|
||||
job,
|
||||
logger,
|
||||
env,
|
||||
cwd,
|
||||
projectRootOverride,
|
||||
}: {
|
||||
job: TJob;
|
||||
logger: bunyan;
|
||||
env: BuildStepEnv;
|
||||
cwd: string;
|
||||
projectRootOverride?: string;
|
||||
}): Promise<{
|
||||
newJob: TJob;
|
||||
newMetadata: Metadata;
|
||||
}> {
|
||||
const { cmd, args, extraEnv } = await resolveEasCommandPrefixAndEnvAsync();
|
||||
const { buildProfile, githubTriggerOptions } = job;
|
||||
assert(buildProfile, 'build profile is missing in a build from git-based integration.');
|
||||
|
||||
const autoSubmitArgs = [];
|
||||
if (githubTriggerOptions?.submitProfile) {
|
||||
autoSubmitArgs.push('--auto-submit-with-profile');
|
||||
autoSubmitArgs.push(githubTriggerOptions.submitProfile);
|
||||
} else if (githubTriggerOptions?.autoSubmit) {
|
||||
autoSubmitArgs.push('--auto-submit');
|
||||
}
|
||||
|
||||
const result = await spawn(
|
||||
cmd,
|
||||
[
|
||||
...args,
|
||||
'build:internal',
|
||||
'--platform',
|
||||
job.platform,
|
||||
'--profile',
|
||||
buildProfile,
|
||||
...autoSubmitArgs,
|
||||
],
|
||||
{
|
||||
cwd,
|
||||
env: {
|
||||
...env,
|
||||
EXPO_TOKEN: nullthrows(job.secrets, 'Secrets must be defined for non-custom builds')
|
||||
.robotAccessToken,
|
||||
...extraEnv,
|
||||
EAS_PROJECT_ROOT: projectRootOverride,
|
||||
},
|
||||
logger,
|
||||
// This prevents printing stdout with job secrets and credentials to logs.
|
||||
mode: PipeMode.STDERR_ONLY_AS_STDOUT,
|
||||
}
|
||||
);
|
||||
|
||||
const stdout = result.stdout.toString();
|
||||
const parsed = JSON.parse(stdout);
|
||||
return validateEasBuildInternalResult({
|
||||
result: parsed,
|
||||
oldJob: job,
|
||||
});
|
||||
}
|
||||
|
||||
export async function resolveEnvFromBuildProfileAsync<TJob extends BuildJob>(
|
||||
ctx: BuildContext<TJob>,
|
||||
{ cwd }: { cwd: string }
|
||||
): Promise<Env> {
|
||||
const { cmd, args, extraEnv } = await resolveEasCommandPrefixAndEnvAsync();
|
||||
const { buildProfile } = ctx.job;
|
||||
assert(buildProfile, 'build profile is missing in a build from git-based integration.');
|
||||
let spawnResult;
|
||||
try {
|
||||
spawnResult = await spawn(
|
||||
cmd,
|
||||
[
|
||||
...args,
|
||||
'config',
|
||||
'--platform',
|
||||
ctx.job.platform,
|
||||
'--profile',
|
||||
buildProfile,
|
||||
'--non-interactive',
|
||||
'--json',
|
||||
'--eas-json-only',
|
||||
],
|
||||
{
|
||||
cwd,
|
||||
env: { ...ctx.env, ...extraEnv },
|
||||
}
|
||||
);
|
||||
} catch (err: any) {
|
||||
ctx.logger.error(`Failed to the read build profile ${buildProfile} from eas.json.`);
|
||||
ctx.logger.error(err.stderr?.toString());
|
||||
throw Error(`Failed to read the build profile ${buildProfile} from eas.json.`);
|
||||
}
|
||||
const stdout = spawnResult.stdout.toString();
|
||||
const parsed = JSON.parse(stdout);
|
||||
const env = validateEnvs(parsed.buildProfile);
|
||||
return env;
|
||||
}
|
||||
|
||||
async function resolveEasCommandPrefixAndEnvAsync(): Promise<{
|
||||
cmd: string;
|
||||
args: string[];
|
||||
extraEnv: Env;
|
||||
}> {
|
||||
const npxArgsPrefix = (await isAtLeastNpm7Async()) ? ['-y'] : [];
|
||||
if (process.env.ENVIRONMENT === 'development') {
|
||||
return {
|
||||
cmd: 'npx',
|
||||
args: [...npxArgsPrefix, `eas-cli@${EasCliNpmTags.STAGING}`],
|
||||
extraEnv: {},
|
||||
};
|
||||
} else if (process.env.ENVIRONMENT === 'staging') {
|
||||
return {
|
||||
cmd: 'npx',
|
||||
args: [...npxArgsPrefix, `eas-cli@${EasCliNpmTags.STAGING}`],
|
||||
extraEnv: { EXPO_STAGING: '1' },
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
cmd: 'npx',
|
||||
args: [...npxArgsPrefix, `eas-cli@${EasCliNpmTags.PRODUCTION}`],
|
||||
extraEnv: {},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function validateEasBuildInternalResult<TJob extends BuildJob>({
|
||||
oldJob,
|
||||
result,
|
||||
}: {
|
||||
oldJob: TJob;
|
||||
result: any;
|
||||
}): { newJob: TJob; newMetadata: Metadata } {
|
||||
const { value, error } = EasBuildInternalResultSchema.validate(result, {
|
||||
stripUnknown: true,
|
||||
convert: true,
|
||||
abortEarly: false,
|
||||
});
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
const newJob = sanitizeBuildJob({
|
||||
...value.job,
|
||||
// We want to retain values that we have set on the job.
|
||||
appId: oldJob.appId,
|
||||
initiatingUserId: oldJob.initiatingUserId,
|
||||
}) as TJob;
|
||||
assert(newJob.platform === oldJob.platform, 'eas-cli returned a job for a wrong platform');
|
||||
const newMetadata = sanitizeMetadata(value.metadata);
|
||||
return { newJob, newMetadata };
|
||||
}
|
||||
|
||||
function validateEnvs(result: any): Env {
|
||||
const { value, error } = Joi.object({
|
||||
env: Joi.object().pattern(Joi.string(), Joi.string()),
|
||||
}).validate(result, {
|
||||
stripUnknown: true,
|
||||
convert: true,
|
||||
abortEarly: false,
|
||||
});
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
return value?.env;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
export const COMMON_FASTLANE_ENV = {
|
||||
FASTLANE_DISABLE_COLORS: '1',
|
||||
FASTLANE_SKIP_UPDATE_CHECK: '1',
|
||||
SKIP_SLOW_FASTLANE_WARNING: 'true',
|
||||
FASTLANE_HIDE_TIMESTAMP: 'true',
|
||||
LC_ALL: 'en_US.UTF-8',
|
||||
};
|
||||
@@ -0,0 +1,98 @@
|
||||
import { ArchiveSource, ArchiveSourceType } from '@expo/eas-build-job';
|
||||
import { bunyan } from '@expo/logger';
|
||||
import spawn from '@expo/turtle-spawn';
|
||||
|
||||
export async function shallowCloneRepositoryAsync({
|
||||
logger,
|
||||
archiveSource,
|
||||
destinationDirectory,
|
||||
}: {
|
||||
logger: bunyan;
|
||||
archiveSource: ArchiveSource & { type: ArchiveSourceType.GIT };
|
||||
destinationDirectory: string;
|
||||
}): Promise<void> {
|
||||
const { repositoryUrl } = archiveSource;
|
||||
try {
|
||||
await spawn('git', ['init'], { cwd: destinationDirectory });
|
||||
await spawn('git', ['remote', 'add', 'origin', repositoryUrl], { cwd: destinationDirectory });
|
||||
|
||||
const { gitRef, gitCommitHash } = archiveSource;
|
||||
|
||||
await spawn('git', ['fetch', 'origin', '--depth', '1', '--no-tags', gitCommitHash], {
|
||||
cwd: destinationDirectory,
|
||||
});
|
||||
|
||||
await spawn('git', ['checkout', gitCommitHash], { cwd: destinationDirectory });
|
||||
|
||||
// If we have a gitRef, we try to add it to the repo.
|
||||
if (gitRef) {
|
||||
const { name, type } = getStrippedBranchOrTagName(gitRef);
|
||||
switch (type) {
|
||||
// If the gitRef is for a tag, we add a lightweight tag to current commit.
|
||||
case 'tag': {
|
||||
await spawn('git', ['tag', name], { cwd: destinationDirectory });
|
||||
break;
|
||||
}
|
||||
// gitRef for a branch may come as:
|
||||
// - qualified ref (e.g. refs/heads/feature/add-icon), detected as "branch" for a push,
|
||||
// - unqualified ref (e.g. feature/add-icon), detected as "other" for a pull request.
|
||||
case 'branch':
|
||||
case 'other': {
|
||||
await spawn('git', ['checkout', '-b', name], { cwd: destinationDirectory });
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err: any) {
|
||||
const sanitizedUrl = getSanitizedGitUrl(repositoryUrl);
|
||||
if (sanitizedUrl) {
|
||||
logger.error(`Failed to clone git repository: ${sanitizedUrl}.`);
|
||||
} else {
|
||||
logger.error('Failed to clone git repository.');
|
||||
}
|
||||
logger.error(err.stderr);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
function getSanitizedGitUrl(maybeGitUrl: string): string | null {
|
||||
try {
|
||||
const url = new URL(maybeGitUrl);
|
||||
if (url.password) {
|
||||
url.password = '*******';
|
||||
}
|
||||
return url.toString();
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function getStrippedBranchOrTagName(ref: string): {
|
||||
name: string;
|
||||
type: 'branch' | 'tag' | 'other';
|
||||
} {
|
||||
const branchRegex = /(\/?refs)?\/?heads\/(.+)/;
|
||||
const branchMatch = ref.match(branchRegex);
|
||||
|
||||
if (branchMatch) {
|
||||
return {
|
||||
name: branchMatch[2],
|
||||
type: 'branch',
|
||||
};
|
||||
}
|
||||
|
||||
const tagRegex = /(\/?refs)?\/?tags\/(.+)/;
|
||||
const tagMatch = ref.match(tagRegex);
|
||||
|
||||
if (tagMatch) {
|
||||
return {
|
||||
name: tagMatch[2],
|
||||
type: 'tag',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
name: ref,
|
||||
type: 'other',
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
import { Job } from '@expo/eas-build-job';
|
||||
import spawn, { SpawnOptions, SpawnPromise, SpawnResult } from '@expo/turtle-spawn';
|
||||
import path from 'path';
|
||||
|
||||
import { BuildContext } from '../context';
|
||||
import { PackageManager, findPackagerRootDir } from '../utils/packageManager';
|
||||
import { isUsingModernYarnVersion } from '../utils/project';
|
||||
|
||||
export async function installDependenciesAsync({
|
||||
packageManager,
|
||||
env,
|
||||
logger,
|
||||
infoCallbackFn,
|
||||
cwd,
|
||||
useFrozenLockfile,
|
||||
}: {
|
||||
packageManager: PackageManager;
|
||||
env: Record<string, string | undefined>;
|
||||
cwd: string;
|
||||
logger: Exclude<SpawnOptions['logger'], undefined>;
|
||||
infoCallbackFn?: SpawnOptions['infoCallbackFn'];
|
||||
useFrozenLockfile: boolean;
|
||||
}): Promise<{ spawnPromise: SpawnPromise<SpawnResult> }> {
|
||||
let args: string[];
|
||||
switch (packageManager) {
|
||||
case PackageManager.NPM: {
|
||||
args = useFrozenLockfile ? ['ci', '--include=dev'] : ['install'];
|
||||
break;
|
||||
}
|
||||
case PackageManager.PNPM: {
|
||||
args = ['install', useFrozenLockfile ? '--frozen-lockfile' : '--no-frozen-lockfile'];
|
||||
break;
|
||||
}
|
||||
case PackageManager.YARN: {
|
||||
const isModernYarnVersion = await isUsingModernYarnVersion(cwd);
|
||||
if (isModernYarnVersion) {
|
||||
if (env['EAS_YARN_FOCUS_WORKSPACE']) {
|
||||
args = ['workspaces', 'focus', env['EAS_YARN_FOCUS_WORKSPACE']];
|
||||
} else {
|
||||
args = [
|
||||
'install',
|
||||
'--inline-builds',
|
||||
useFrozenLockfile ? '--immutable' : '--no-immutable',
|
||||
];
|
||||
}
|
||||
} else {
|
||||
args = ['install', ...(useFrozenLockfile ? ['--frozen-lockfile'] : [])];
|
||||
}
|
||||
break;
|
||||
}
|
||||
case PackageManager.BUN:
|
||||
args = ['install', ...(useFrozenLockfile ? ['--frozen-lockfile'] : [])];
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unsupported package manager: ${packageManager}`);
|
||||
}
|
||||
if (env['EAS_VERBOSE'] === '1') {
|
||||
args = [...args, '--verbose'];
|
||||
}
|
||||
logger.info(`Running "${packageManager} ${args.join(' ')}" in ${cwd} directory`);
|
||||
return {
|
||||
spawnPromise: spawn(packageManager, args, {
|
||||
cwd,
|
||||
logger,
|
||||
infoCallbackFn,
|
||||
env,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
export function resolvePackagerDir(ctx: BuildContext<Job>): string {
|
||||
const packagerRunDir = findPackagerRootDir(ctx.getReactNativeProjectDirectory());
|
||||
if (packagerRunDir !== ctx.getReactNativeProjectDirectory()) {
|
||||
const relativeReactNativeProjectDirectory = path.relative(
|
||||
ctx.buildDirectory,
|
||||
ctx.getReactNativeProjectDirectory()
|
||||
);
|
||||
ctx.logger.info(
|
||||
`We detected that '${relativeReactNativeProjectDirectory}' is a ${ctx.packageManager} workspace`
|
||||
);
|
||||
}
|
||||
return packagerRunDir;
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import { BuildJob } from '@expo/eas-build-job';
|
||||
import { bunyan } from '@expo/logger';
|
||||
import { SpawnOptions } from '@expo/turtle-spawn';
|
||||
|
||||
import { installDependenciesAsync, resolvePackagerDir } from './installDependencies';
|
||||
import { BuildContext } from '../context';
|
||||
import { runExpoCliCommand } from '../utils/project';
|
||||
|
||||
export interface PrebuildOptions {
|
||||
extraEnvs?: Record<string, string>;
|
||||
}
|
||||
|
||||
export async function prebuildAsync<TJob extends BuildJob>(
|
||||
ctx: BuildContext<TJob>,
|
||||
{ logger, workingDir, options }: { logger: bunyan; workingDir: string; options?: PrebuildOptions }
|
||||
): Promise<void> {
|
||||
const spawnOptions: SpawnOptions = {
|
||||
cwd: workingDir,
|
||||
logger,
|
||||
env: {
|
||||
EXPO_IMAGE_UTILS_NO_SHARP: '1',
|
||||
...options?.extraEnvs,
|
||||
...ctx.env,
|
||||
},
|
||||
};
|
||||
|
||||
const prebuildCommandArgs = getPrebuildCommandArgs(ctx);
|
||||
await runExpoCliCommand({
|
||||
args: prebuildCommandArgs,
|
||||
options: spawnOptions,
|
||||
packageManager: ctx.packageManager,
|
||||
});
|
||||
const installDependenciesSpawnPromise = (
|
||||
await installDependenciesAsync({
|
||||
packageManager: ctx.packageManager,
|
||||
env: ctx.env,
|
||||
logger,
|
||||
cwd: resolvePackagerDir(ctx),
|
||||
// prebuild sometimes modifies package.json, so we don't want to use frozen lockfile
|
||||
useFrozenLockfile: false,
|
||||
})
|
||||
).spawnPromise;
|
||||
await installDependenciesSpawnPromise;
|
||||
}
|
||||
|
||||
function getPrebuildCommandArgs<TJob extends BuildJob>(ctx: BuildContext<TJob>): string[] {
|
||||
let prebuildCommand =
|
||||
ctx.job.experimental?.prebuildCommand ?? `prebuild --no-install --platform ${ctx.job.platform}`;
|
||||
if (!prebuildCommand.match(/(?:--platform| -p)/)) {
|
||||
prebuildCommand = `${prebuildCommand} --platform ${ctx.job.platform}`;
|
||||
}
|
||||
const npxCommandPrefix = 'npx ';
|
||||
const expoCommandPrefix = 'expo ';
|
||||
const expoCliCommandPrefix = 'expo-cli ';
|
||||
if (prebuildCommand.startsWith(npxCommandPrefix)) {
|
||||
prebuildCommand = prebuildCommand.substring(npxCommandPrefix.length).trim();
|
||||
}
|
||||
if (prebuildCommand.startsWith(expoCommandPrefix)) {
|
||||
prebuildCommand = prebuildCommand.substring(expoCommandPrefix.length).trim();
|
||||
}
|
||||
if (prebuildCommand.startsWith(expoCliCommandPrefix)) {
|
||||
prebuildCommand = prebuildCommand.substring(expoCliCommandPrefix.length).trim();
|
||||
}
|
||||
|
||||
return prebuildCommand.split(' ');
|
||||
}
|
||||
@@ -0,0 +1,296 @@
|
||||
import downloadFile from '@expo/downloader';
|
||||
import { ArchiveSource, ArchiveSourceSchemaZ, ArchiveSourceType, Job } from '@expo/eas-build-job';
|
||||
import { bunyan } from '@expo/logger';
|
||||
import { asyncResult } from '@expo/results';
|
||||
import spawn from '@expo/turtle-spawn';
|
||||
import fs from 'fs/promises';
|
||||
import { graphql } from 'gql.tada';
|
||||
import fetch from 'node-fetch';
|
||||
import nullthrows from 'nullthrows';
|
||||
import path from 'path';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { shallowCloneRepositoryAsync } from './git';
|
||||
import { BuildContext } from '../context';
|
||||
import { turtleFetch } from '../utils/turtleFetch';
|
||||
|
||||
export async function prepareProjectSourcesAsync<TJob extends Job>(
|
||||
ctx: BuildContext<TJob>,
|
||||
destinationDirectory: string
|
||||
): // Return type required to make switch exhaustive.
|
||||
Promise<{ handled: boolean }> {
|
||||
let projectArchive: ArchiveSource = ctx.job.projectArchive;
|
||||
if (ctx.isLocal) {
|
||||
console.warn('Local build, skipping project archive refresh');
|
||||
} else {
|
||||
const projectArchiveResult = await asyncResult(fetchProjectArchiveSourceAsync(ctx));
|
||||
|
||||
if (!projectArchiveResult.ok) {
|
||||
ctx.logger.error(
|
||||
{ err: projectArchiveResult.reason },
|
||||
'Failed to refresh project archive, falling back to the original one'
|
||||
);
|
||||
}
|
||||
|
||||
projectArchive = projectArchiveResult.value ?? ctx.job.projectArchive;
|
||||
}
|
||||
|
||||
switch (projectArchive.type) {
|
||||
case ArchiveSourceType.R2:
|
||||
case ArchiveSourceType.GCS: {
|
||||
throw new Error('Remote project sources should be resolved earlier to URL');
|
||||
}
|
||||
|
||||
case ArchiveSourceType.PATH: {
|
||||
await prepareProjectSourcesLocallyAsync(ctx, projectArchive.path, destinationDirectory); // used in eas build --local
|
||||
return { handled: true };
|
||||
}
|
||||
|
||||
case ArchiveSourceType.NONE: {
|
||||
// May be used in no-sources jobs like submission jobs.
|
||||
return { handled: true };
|
||||
}
|
||||
|
||||
case ArchiveSourceType.URL: {
|
||||
await downloadAndUnpackProjectFromTarGzAsync(ctx, projectArchive.url, destinationDirectory);
|
||||
|
||||
uploadProjectMetadataAsFireAndForget(ctx, { projectDirectory: destinationDirectory });
|
||||
|
||||
return { handled: true };
|
||||
}
|
||||
|
||||
case ArchiveSourceType.GIT: {
|
||||
await shallowCloneRepositoryAsync({
|
||||
logger: ctx.logger,
|
||||
archiveSource: projectArchive,
|
||||
destinationDirectory,
|
||||
});
|
||||
|
||||
uploadProjectMetadataAsFireAndForget(ctx, { projectDirectory: destinationDirectory });
|
||||
|
||||
return { handled: true };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function downloadAndUnpackProjectFromTarGzAsync<TJob extends Job>(
|
||||
ctx: BuildContext<TJob>,
|
||||
projectArchiveUrl: string,
|
||||
destinationDirectory: string
|
||||
): Promise<void> {
|
||||
const projectTarball = path.join(ctx.workingdir, 'project.tar.gz');
|
||||
try {
|
||||
await downloadFile(projectArchiveUrl, projectTarball, { retry: 3 });
|
||||
} catch (err: any) {
|
||||
ctx.reportError?.('Failed to download project archive', err, {
|
||||
extras: { buildId: ctx.env.EAS_BUILD_ID },
|
||||
});
|
||||
throw err;
|
||||
}
|
||||
|
||||
await unpackTarGzAsync({
|
||||
destination: destinationDirectory,
|
||||
source: projectTarball,
|
||||
logger: ctx.logger,
|
||||
});
|
||||
}
|
||||
|
||||
async function prepareProjectSourcesLocallyAsync<TJob extends Job>(
|
||||
ctx: BuildContext<TJob>,
|
||||
projectArchivePath: string,
|
||||
destinationDirectory: string
|
||||
): Promise<void> {
|
||||
const projectTarball = path.join(ctx.workingdir, 'project.tar.gz');
|
||||
await fs.copyFile(projectArchivePath, projectTarball);
|
||||
|
||||
await unpackTarGzAsync({
|
||||
destination: destinationDirectory,
|
||||
source: projectTarball,
|
||||
logger: ctx.logger,
|
||||
});
|
||||
}
|
||||
|
||||
async function unpackTarGzAsync({
|
||||
logger,
|
||||
source,
|
||||
destination,
|
||||
}: {
|
||||
logger: bunyan;
|
||||
source: string;
|
||||
destination: string;
|
||||
}): Promise<void> {
|
||||
await spawn('tar', ['-C', destination, '--strip-components', '1', '-zxf', source], {
|
||||
logger,
|
||||
});
|
||||
}
|
||||
|
||||
function uploadProjectMetadataAsFireAndForget(
|
||||
ctx: BuildContext<Job>,
|
||||
{ projectDirectory }: { projectDirectory: string }
|
||||
): void {
|
||||
void (async () => {
|
||||
const uploadResult = await asyncResult(uploadProjectMetadataAsync(ctx, { projectDirectory }));
|
||||
if (!uploadResult.ok) {
|
||||
ctx.logger.warn(`Failed to upload project metadata: ${uploadResult.reason}`);
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
async function uploadProjectMetadataAsync(
|
||||
ctx: BuildContext<Job>,
|
||||
{ projectDirectory }: { projectDirectory: string }
|
||||
): Promise<void> {
|
||||
if (!ctx.job.platform) {
|
||||
// Not a build job, skip.
|
||||
return;
|
||||
} else if (
|
||||
ctx.job.projectArchive.type === ArchiveSourceType.GCS &&
|
||||
ctx.job.projectArchive.metadataLocation
|
||||
) {
|
||||
// Build already has project metadata, skip.
|
||||
return;
|
||||
}
|
||||
|
||||
const files: string[] = [];
|
||||
|
||||
const directoriesToScan: { dir: string; relativePath: string }[] = [
|
||||
{ dir: projectDirectory, relativePath: '' },
|
||||
];
|
||||
|
||||
while (directoriesToScan.length > 0) {
|
||||
const { dir, relativePath } = directoriesToScan.shift()!;
|
||||
|
||||
if (relativePath === '.git') {
|
||||
// Do not include whole `.git` directory in the archive, just that it exists.
|
||||
files.push('.git/...');
|
||||
continue;
|
||||
}
|
||||
|
||||
const entries = await fs.readdir(dir, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
const fullPath = path.join(dir, entry.name);
|
||||
const relativeFilePath = path.join(relativePath, entry.name);
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
directoriesToScan.push({ dir: fullPath, relativePath: relativeFilePath });
|
||||
} else {
|
||||
files.push(relativeFilePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
const sortedFiles = files
|
||||
.map(
|
||||
// Prepend entries with "project/"
|
||||
f => path.join('project', f)
|
||||
)
|
||||
.sort(); // Sort for consistent ordering
|
||||
|
||||
const result = await ctx.graphqlClient
|
||||
.mutation(
|
||||
graphql(`
|
||||
mutation {
|
||||
uploadSession {
|
||||
createUploadSession(type: EAS_BUILD_GCS_PROJECT_METADATA)
|
||||
}
|
||||
}
|
||||
`),
|
||||
{}
|
||||
)
|
||||
.toPromise();
|
||||
|
||||
if (result.error) {
|
||||
throw result.error;
|
||||
}
|
||||
|
||||
const uploadSession = result.data!.uploadSession.createUploadSession as {
|
||||
url: string;
|
||||
bucketKey: string;
|
||||
headers: Record<string, string>;
|
||||
};
|
||||
|
||||
await fetch(uploadSession.url, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ archiveContent: sortedFiles }),
|
||||
headers: uploadSession.headers,
|
||||
});
|
||||
|
||||
const updateMetadataResult = await ctx.graphqlClient
|
||||
.mutation(
|
||||
graphql(`
|
||||
mutation UpdateTurtleBuildMetadataMutation(
|
||||
$buildId: ID!
|
||||
$projectMetadataFile: ProjectMetadataFileInput!
|
||||
) {
|
||||
build {
|
||||
updateBuildMetadata(
|
||||
buildId: $buildId
|
||||
metadata: { projectMetadataFile: $projectMetadataFile }
|
||||
) {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
`),
|
||||
{
|
||||
buildId: ctx.env.EAS_BUILD_ID,
|
||||
projectMetadataFile: {
|
||||
type: 'GCS',
|
||||
bucketKey: uploadSession.bucketKey,
|
||||
},
|
||||
}
|
||||
)
|
||||
.toPromise();
|
||||
|
||||
if (updateMetadataResult.error) {
|
||||
throw updateMetadataResult.error;
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchProjectArchiveSourceAsync(ctx: BuildContext<Job>): Promise<ArchiveSource> {
|
||||
const taskId = nullthrows(ctx.env.EAS_BUILD_ID, 'EAS_BUILD_ID is not set');
|
||||
const expoApiServerURL = nullthrows(ctx.env.__API_SERVER_URL, '__API_SERVER_URL is not set');
|
||||
const robotAccessToken = nullthrows(
|
||||
ctx.job.secrets?.robotAccessToken,
|
||||
'robot access token is not set'
|
||||
);
|
||||
|
||||
const response = await turtleFetch(
|
||||
new URL(
|
||||
ctx.job.platform
|
||||
? `/v2/turtle-builds/${taskId}/download-project-archive`
|
||||
: `/v2/turtle-job-runs/${taskId}/download-project-archive`,
|
||||
expoApiServerURL
|
||||
).toString(),
|
||||
'POST',
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${robotAccessToken}`,
|
||||
},
|
||||
timeout: 20000,
|
||||
retries: 3,
|
||||
logger: ctx.logger,
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
const textResult = await asyncResult(response.text());
|
||||
throw new Error(`Unexpected response from server (${response.status}): ${textResult.value}`);
|
||||
}
|
||||
|
||||
const jsonResult = await asyncResult(response.json());
|
||||
if (!jsonResult.ok) {
|
||||
throw new Error(
|
||||
`Expected JSON response from server (${response.status}): ${jsonResult.reason}`
|
||||
);
|
||||
}
|
||||
|
||||
const dataResult = z.object({ data: ArchiveSourceSchemaZ }).safeParse(jsonResult.value);
|
||||
if (!dataResult.success) {
|
||||
throw new Error(
|
||||
`Unexpected data from server (${response.status}): ${z.prettifyError(dataResult.error)}`
|
||||
);
|
||||
}
|
||||
|
||||
return dataResult.data.data;
|
||||
}
|
||||
@@ -0,0 +1,292 @@
|
||||
import { ExpoConfig } from '@expo/config';
|
||||
import { BuildJob, BuildPhase, Ios, Job, Platform } from '@expo/eas-build-job';
|
||||
import { BuildTrigger } from '@expo/eas-build-job/dist/common';
|
||||
import { UserFacingError } from '@expo/eas-build-job/dist/errors';
|
||||
import spawn, { SpawnResult } from '@expo/turtle-spawn';
|
||||
import fs from 'fs-extra';
|
||||
import nullthrows from 'nullthrows';
|
||||
import path from 'path';
|
||||
|
||||
import { resolveEnvFromBuildProfileAsync, runEasBuildInternalAsync } from './easBuildInternal';
|
||||
import { installDependenciesAsync, resolvePackagerDir } from './installDependencies';
|
||||
import { prepareProjectSourcesAsync } from './projectSources';
|
||||
import { BuildContext } from '../context';
|
||||
import { deleteXcodeEnvLocalIfExistsAsync } from '../ios/xcodeEnv';
|
||||
import { Hook, runHookIfPresent } from '../utils/hooks';
|
||||
import { setUpNpmrcAsync } from '../utils/npmrc';
|
||||
import {
|
||||
getPackageVersionFromPackageJson,
|
||||
isAtLeastNpm7Async,
|
||||
shouldUseFrozenLockfile,
|
||||
} from '../utils/packageManager';
|
||||
import { getParentAndDescendantProcessPidsAsync } from '../utils/processes';
|
||||
import { readEasJsonContents, readPackageJson } from '../utils/project';
|
||||
import { retryAsync } from '../utils/retry';
|
||||
|
||||
const MAX_EXPO_DOCTOR_TIMEOUT_MS = 30 * 1000;
|
||||
const INSTALL_DEPENDENCIES_WARN_TIMEOUT_MS = 15 * 60 * 1000;
|
||||
const INSTALL_DEPENDENCIES_KILL_TIMEOUT_MS = 30 * 60 * 1000;
|
||||
|
||||
class DoctorTimeoutError extends Error {}
|
||||
class InstallDependenciesTimeoutError extends Error {}
|
||||
|
||||
export async function setupAsync<TJob extends BuildJob>(ctx: BuildContext<TJob>): Promise<void> {
|
||||
await ctx.runBuildPhase(BuildPhase.PREPARE_PROJECT, async () => {
|
||||
await retryAsync(
|
||||
async () => {
|
||||
await fs.rm(ctx.buildDirectory, { recursive: true, force: true });
|
||||
await fs.mkdir(ctx.buildDirectory, { recursive: true });
|
||||
|
||||
await prepareProjectSourcesAsync(ctx, ctx.buildDirectory);
|
||||
},
|
||||
{
|
||||
retryOptions: {
|
||||
retries: 3,
|
||||
retryIntervalMs: 1_000,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
await setUpNpmrcAsync(ctx, ctx.logger);
|
||||
if (ctx.job.platform === Platform.IOS && ctx.env.EAS_BUILD_RUNNER === 'eas-build') {
|
||||
await deleteXcodeEnvLocalIfExistsAsync(ctx as BuildContext<Ios.Job>);
|
||||
}
|
||||
if (ctx.job.triggeredBy === BuildTrigger.GIT_BASED_INTEGRATION) {
|
||||
// We need to setup envs from eas.json before
|
||||
// eas-build-pre-install hook is called.
|
||||
const env = await resolveEnvFromBuildProfileAsync(ctx, {
|
||||
cwd: ctx.getReactNativeProjectDirectory(),
|
||||
});
|
||||
ctx.updateEnv(env);
|
||||
}
|
||||
});
|
||||
|
||||
await ctx.runBuildPhase(BuildPhase.PRE_INSTALL_HOOK, async () => {
|
||||
await runHookIfPresent(ctx, Hook.PRE_INSTALL);
|
||||
});
|
||||
|
||||
await ctx.runBuildPhase(BuildPhase.READ_EAS_JSON, async () => {
|
||||
try {
|
||||
const easJsonContents = readEasJsonContents(ctx.getReactNativeProjectDirectory());
|
||||
ctx.logger.info('Using eas.json:');
|
||||
ctx.logger.info(easJsonContents);
|
||||
} catch (err) {
|
||||
ctx.logger.error({ err }, `Failed to read eas.json.`);
|
||||
}
|
||||
});
|
||||
|
||||
const packageJson = await ctx.runBuildPhase(BuildPhase.READ_PACKAGE_JSON, async () => {
|
||||
ctx.logger.info('Using package.json:');
|
||||
const packageJson = readPackageJson(ctx.getReactNativeProjectDirectory());
|
||||
ctx.logger.info(JSON.stringify(packageJson, null, 2));
|
||||
return packageJson;
|
||||
});
|
||||
|
||||
await ctx.runBuildPhase(BuildPhase.INSTALL_DEPENDENCIES, async () => {
|
||||
const expoVersion =
|
||||
ctx.metadata?.sdkVersion ??
|
||||
getPackageVersionFromPackageJson({
|
||||
packageJson,
|
||||
packageName: 'expo',
|
||||
});
|
||||
|
||||
const reactNativeVersion =
|
||||
ctx.metadata?.reactNativeVersion ??
|
||||
getPackageVersionFromPackageJson({
|
||||
packageJson,
|
||||
packageName: 'react-native',
|
||||
});
|
||||
|
||||
await runInstallDependenciesAsync(ctx, {
|
||||
useFrozenLockfile: shouldUseFrozenLockfile({
|
||||
env: ctx.env,
|
||||
sdkVersion: expoVersion,
|
||||
reactNativeVersion,
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
await ctx.runBuildPhase(BuildPhase.READ_APP_CONFIG, async () => {
|
||||
const appConfig = ctx.appConfig;
|
||||
ctx.logger.info('Using app configuration:');
|
||||
ctx.logger.info(JSON.stringify(appConfig, null, 2));
|
||||
await validateAppConfigAsync(ctx, appConfig);
|
||||
});
|
||||
|
||||
if (ctx.job.triggeredBy === BuildTrigger.GIT_BASED_INTEGRATION) {
|
||||
await ctx.runBuildPhase(BuildPhase.EAS_BUILD_INTERNAL, async () => {
|
||||
if (!ctx.appConfig.ios?.bundleIdentifier && ctx.job.platform === Platform.IOS) {
|
||||
throw new Error(
|
||||
'The "ios.bundleIdentifier" is required to be set in app config for builds triggered by GitHub integration. Learn more: https://docs.expo.dev/versions/latest/config/app/#bundleidentifier.'
|
||||
);
|
||||
}
|
||||
if (!ctx.appConfig.android?.package && ctx.job.platform === Platform.ANDROID) {
|
||||
throw new Error(
|
||||
'The "android.package" is required to be set in app config for builds triggered by GitHub integration. Learn more: https://docs.expo.dev/versions/latest/config/app/#package.'
|
||||
);
|
||||
}
|
||||
const { newJob, newMetadata } = await runEasBuildInternalAsync({
|
||||
job: ctx.job,
|
||||
env: ctx.env,
|
||||
logger: ctx.logger,
|
||||
cwd: ctx.getReactNativeProjectDirectory(),
|
||||
projectRootOverride: ctx.env.EAS_NO_VCS ? ctx.buildDirectory : undefined,
|
||||
});
|
||||
ctx.updateJobInformation(newJob, newMetadata);
|
||||
});
|
||||
}
|
||||
|
||||
const hasExpoPackage = !!packageJson.dependencies?.expo;
|
||||
if (!ctx.env.EAS_BUILD_DISABLE_EXPO_DOCTOR_STEP && hasExpoPackage) {
|
||||
await ctx.runBuildPhase(BuildPhase.RUN_EXPO_DOCTOR, async () => {
|
||||
try {
|
||||
await runExpoDoctor(ctx);
|
||||
} catch (err) {
|
||||
if (err instanceof DoctorTimeoutError) {
|
||||
ctx.logger.error(err.message);
|
||||
} else {
|
||||
ctx.logger.error({ err }, 'Command "expo doctor" failed.');
|
||||
}
|
||||
ctx.markBuildPhaseHasWarnings();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function runExpoDoctor<TJob extends Job>(ctx: BuildContext<TJob>): Promise<SpawnResult> {
|
||||
ctx.logger.info('Running "expo doctor"');
|
||||
let timeout: NodeJS.Timeout | undefined;
|
||||
let timedOut = false;
|
||||
const isAtLeastNpm7 = await isAtLeastNpm7Async();
|
||||
try {
|
||||
const argsPrefix = isAtLeastNpm7 ? ['-y'] : [];
|
||||
const promise = spawn('npx', [...argsPrefix, 'expo-doctor'], {
|
||||
cwd: ctx.getReactNativeProjectDirectory(),
|
||||
logger: ctx.logger,
|
||||
env: ctx.env,
|
||||
});
|
||||
timeout = setTimeout(async () => {
|
||||
timedOut = true;
|
||||
const ppid = nullthrows(promise.child.pid);
|
||||
const pids = await getParentAndDescendantProcessPidsAsync(ppid);
|
||||
pids.forEach(pid => {
|
||||
process.kill(pid);
|
||||
});
|
||||
ctx.reportError?.(`"expo doctor" timed out`, undefined, {
|
||||
extras: { buildId: ctx.env.EAS_BUILD_ID },
|
||||
});
|
||||
}, MAX_EXPO_DOCTOR_TIMEOUT_MS);
|
||||
return await promise;
|
||||
} catch (err: any) {
|
||||
if (timedOut) {
|
||||
throw new DoctorTimeoutError('"expo doctor" timed out, skipping...');
|
||||
}
|
||||
throw err;
|
||||
} finally {
|
||||
if (timeout) {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function runInstallDependenciesAsync<TJob extends Job>(
|
||||
ctx: BuildContext<TJob>,
|
||||
{
|
||||
useFrozenLockfile,
|
||||
}: {
|
||||
useFrozenLockfile: boolean;
|
||||
}
|
||||
): Promise<void> {
|
||||
let warnTimeout: NodeJS.Timeout | undefined;
|
||||
let killTimeout: NodeJS.Timeout | undefined;
|
||||
let killTimedOut: boolean = false;
|
||||
try {
|
||||
const installDependenciesSpawnPromise = (
|
||||
await installDependenciesAsync({
|
||||
packageManager: ctx.packageManager,
|
||||
env: ctx.env,
|
||||
logger: ctx.logger,
|
||||
infoCallbackFn: () => {
|
||||
if (warnTimeout) {
|
||||
warnTimeout.refresh();
|
||||
}
|
||||
if (killTimeout) {
|
||||
killTimeout.refresh();
|
||||
}
|
||||
},
|
||||
cwd: resolvePackagerDir(ctx),
|
||||
useFrozenLockfile,
|
||||
})
|
||||
).spawnPromise;
|
||||
|
||||
warnTimeout = setTimeout(() => {
|
||||
ctx.logger.warn(
|
||||
'"Install dependencies" phase takes longer then expected and it did not produce any logs in the past 15 minutes. Consider evaluating your package.json file for possible issues with dependencies'
|
||||
);
|
||||
}, INSTALL_DEPENDENCIES_WARN_TIMEOUT_MS);
|
||||
|
||||
killTimeout = setTimeout(async () => {
|
||||
killTimedOut = true;
|
||||
ctx.logger.error(
|
||||
'"Install dependencies" phase takes a very long time and it did not produce any logs in the past 30 minutes. Most likely an unexpected error happened with your dependencies which caused the process to hang and it will be terminated'
|
||||
);
|
||||
const ppid = nullthrows(installDependenciesSpawnPromise.child.pid);
|
||||
const pids = await getParentAndDescendantProcessPidsAsync(ppid);
|
||||
pids.forEach(pid => {
|
||||
process.kill(pid);
|
||||
});
|
||||
ctx.reportError?.('"Install dependencies" phase takes a very long time', undefined, {
|
||||
extras: { buildId: ctx.env.EAS_BUILD_ID },
|
||||
});
|
||||
}, INSTALL_DEPENDENCIES_KILL_TIMEOUT_MS);
|
||||
|
||||
await installDependenciesSpawnPromise;
|
||||
} catch (err: any) {
|
||||
if (killTimedOut) {
|
||||
throw new InstallDependenciesTimeoutError(
|
||||
'"Install dependencies" phase was inactive for over 30 minutes. Please evaluate your package.json file'
|
||||
);
|
||||
}
|
||||
throw err;
|
||||
} finally {
|
||||
if (warnTimeout) {
|
||||
clearTimeout(warnTimeout);
|
||||
}
|
||||
if (killTimeout) {
|
||||
clearTimeout(killTimeout);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function validateAppConfigAsync(
|
||||
ctx: BuildContext<Job>,
|
||||
appConfig: ExpoConfig
|
||||
): Promise<void> {
|
||||
if (
|
||||
appConfig?.extra?.eas?.projectId &&
|
||||
ctx.env.EAS_BUILD_PROJECT_ID &&
|
||||
appConfig.extra.eas.projectId !== ctx.env.EAS_BUILD_PROJECT_ID
|
||||
) {
|
||||
const isUsingDynamicConfig =
|
||||
(await fs.pathExists(path.join(ctx.getReactNativeProjectDirectory(), 'app.config.ts'))) ||
|
||||
(await fs.pathExists(path.join(ctx.getReactNativeProjectDirectory(), 'app.config.js')));
|
||||
const isGitHubBuild = ctx.job.triggeredBy === BuildTrigger.GIT_BASED_INTEGRATION;
|
||||
let extraMessage = '';
|
||||
if (isGitHubBuild && isUsingDynamicConfig) {
|
||||
extraMessage =
|
||||
'Make sure you connected your GitHub repository to the correct Expo project and if you are using environment variables to switch between projects in app.config.js/app.config.ts remember to set those variables in eas.json too. ';
|
||||
} else if (isGitHubBuild) {
|
||||
extraMessage = 'Make sure you connected your GitHub repository to the correct Expo project. ';
|
||||
} else if (isUsingDynamicConfig) {
|
||||
extraMessage =
|
||||
'If you are using environment variables to switch between projects in app.config.js/app.config.ts, make sure those variables are also set inside EAS Build. You can do that using "env" field in eas.json or EAS environment variables. ';
|
||||
}
|
||||
throw new UserFacingError(
|
||||
'EAS_BUILD_PROJECT_ID_MISMATCH',
|
||||
`The value of the "extra.eas.projectId" field (${appConfig.extra.eas.projectId}) in the app config does not match the current project id (${ctx.env.EAS_BUILD_PROJECT_ID}). ${extraMessage}Learn more: https://expo.fyi/eas-config-mismatch.`
|
||||
);
|
||||
} else if (ctx.env.EAS_BUILD_PROJECT_ID && !appConfig?.extra?.eas?.projectId) {
|
||||
ctx.logger.error(`The "extra.eas.projectId" field is missing from your app config.`);
|
||||
ctx.markBuildPhaseHasWarnings();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,410 @@
|
||||
import { ExpoConfig } from '@expo/config';
|
||||
import {
|
||||
BuildPhase,
|
||||
BuildPhaseResult,
|
||||
BuildPhaseStats,
|
||||
Env,
|
||||
EnvironmentSecretType,
|
||||
GenericArtifactType,
|
||||
Job,
|
||||
LogMarker,
|
||||
ManagedArtifactType,
|
||||
Metadata,
|
||||
errors,
|
||||
isGenericArtifact,
|
||||
} from '@expo/eas-build-job';
|
||||
import { BuildTrigger } from '@expo/eas-build-job/dist/common';
|
||||
import { bunyan } from '@expo/logger';
|
||||
import { Client, fetchExchange } from '@urql/core';
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
|
||||
import { resolveBuildPhaseErrorAsync } from './buildErrors/detectError';
|
||||
import { readAppConfig } from './utils/appConfig';
|
||||
import { createTemporaryEnvironmentSecretFile } from './utils/environmentSecrets';
|
||||
import { PackageManager, resolvePackageManager } from './utils/packageManager';
|
||||
|
||||
export type Artifacts = Partial<Record<ManagedArtifactType, string>>;
|
||||
|
||||
export interface CacheManager {
|
||||
saveCache(ctx: BuildContext<Job>): Promise<void>;
|
||||
restoreCache(ctx: BuildContext<Job>): Promise<void>;
|
||||
}
|
||||
|
||||
export interface LogBuffer {
|
||||
getLogs(): string[];
|
||||
getPhaseLogs(buildPhase: string): string[];
|
||||
}
|
||||
|
||||
export type ArtifactToUpload =
|
||||
| {
|
||||
type: ManagedArtifactType;
|
||||
paths: string[];
|
||||
}
|
||||
| {
|
||||
type: GenericArtifactType;
|
||||
name: string;
|
||||
paths: string[];
|
||||
};
|
||||
|
||||
export interface BuildContextOptions {
|
||||
workingdir: string;
|
||||
logger: bunyan;
|
||||
logBuffer: LogBuffer;
|
||||
env: Env;
|
||||
cacheManager?: CacheManager;
|
||||
uploadArtifact: (spec: { artifact: ArtifactToUpload; logger: bunyan }) => Promise<
|
||||
/** Workflow Artifact ID */
|
||||
| { artifactId: string | null; filename?: never }
|
||||
/** This remains from the time we relied on Launcher to rename the GCS object. */
|
||||
| { artifactId?: never; filename: string | null }
|
||||
>;
|
||||
reportError?: (
|
||||
msg: string,
|
||||
err?: Error,
|
||||
options?: { tags?: Record<string, string>; extras?: Record<string, string> }
|
||||
) => void;
|
||||
reportBuildPhaseStats?: (stats: BuildPhaseStats) => void;
|
||||
skipNativeBuild?: boolean;
|
||||
metadata?: Metadata;
|
||||
}
|
||||
|
||||
export class SkipNativeBuildError extends Error {}
|
||||
|
||||
export class BuildContext<TJob extends Job = Job> {
|
||||
public readonly workingdir: string;
|
||||
public logger: bunyan;
|
||||
public readonly logBuffer: LogBuffer;
|
||||
public readonly cacheManager?: CacheManager;
|
||||
public readonly reportError?: (
|
||||
msg: string,
|
||||
err?: Error,
|
||||
options?: { tags?: Record<string, string>; extras?: Record<string, string> }
|
||||
) => void;
|
||||
public readonly skipNativeBuild?: boolean;
|
||||
public artifacts: Artifacts = {};
|
||||
|
||||
private readonly _isLocal: boolean;
|
||||
|
||||
private _env: Env;
|
||||
private _job: TJob;
|
||||
private _metadata?: Metadata;
|
||||
private readonly defaultLogger: bunyan;
|
||||
private readonly _uploadArtifact: BuildContextOptions['uploadArtifact'];
|
||||
private buildPhase?: BuildPhase;
|
||||
private buildPhaseSkipped = false;
|
||||
private buildPhaseHasWarnings = false;
|
||||
private _appConfig?: ExpoConfig;
|
||||
private readonly reportBuildPhaseStats?: (stats: BuildPhaseStats) => void;
|
||||
public readonly graphqlClient: Client;
|
||||
|
||||
constructor(job: TJob, options: BuildContextOptions) {
|
||||
this.workingdir = options.workingdir;
|
||||
this.defaultLogger = options.logger;
|
||||
this.logger = this.defaultLogger;
|
||||
this.logBuffer = options.logBuffer;
|
||||
this.cacheManager = options.cacheManager;
|
||||
this._uploadArtifact = options.uploadArtifact;
|
||||
this.reportError = options.reportError;
|
||||
|
||||
this._job = job;
|
||||
this._metadata = options.metadata;
|
||||
this.skipNativeBuild = options.skipNativeBuild;
|
||||
this.reportBuildPhaseStats = options.reportBuildPhaseStats;
|
||||
|
||||
const environmentSecrets = this.getEnvironmentSecrets(job);
|
||||
this._env = {
|
||||
...options.env,
|
||||
...job?.builderEnvironment?.env,
|
||||
...environmentSecrets,
|
||||
__EAS_BUILD_ENVS_DIR: this.buildEnvsDirectory,
|
||||
};
|
||||
|
||||
this._env.PATH = this._env.PATH
|
||||
? [this.buildExecutablesDirectory, this._env.PATH].join(':')
|
||||
: this.buildExecutablesDirectory;
|
||||
|
||||
this._isLocal = this._env.EAS_BUILD_RUNNER !== 'eas-build';
|
||||
|
||||
this.graphqlClient = new Client({
|
||||
url: new URL('graphql', this.env.__API_SERVER_URL).toString(),
|
||||
exchanges: [fetchExchange],
|
||||
preferGetMethod: false,
|
||||
fetchOptions: {
|
||||
headers: {
|
||||
Authorization: `Bearer ${job.secrets?.robotAccessToken}`,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
public get job(): TJob {
|
||||
return this._job;
|
||||
}
|
||||
public get metadata(): Metadata | undefined {
|
||||
return this._metadata;
|
||||
}
|
||||
public get env(): Env {
|
||||
return this._env;
|
||||
}
|
||||
public get buildDirectory(): string {
|
||||
return path.join(this.workingdir, 'build');
|
||||
}
|
||||
public get buildLogsDirectory(): string {
|
||||
return path.join(this.workingdir, 'logs');
|
||||
}
|
||||
public get isLocal(): boolean {
|
||||
return this._isLocal;
|
||||
}
|
||||
/**
|
||||
* Directory used to store executables used during regular (non-custom) builds.
|
||||
*/
|
||||
public get buildExecutablesDirectory(): string {
|
||||
return path.join(this.workingdir, 'bin');
|
||||
}
|
||||
/**
|
||||
* Directory used to store env variables registered in the current build step.
|
||||
* All values stored here will be available in the next build phase as env variables.
|
||||
*/
|
||||
public get buildEnvsDirectory(): string {
|
||||
return path.join(this.workingdir, 'env');
|
||||
}
|
||||
public get packageManager(): PackageManager {
|
||||
return resolvePackageManager(this.getReactNativeProjectDirectory());
|
||||
}
|
||||
public get appConfig(): ExpoConfig {
|
||||
if (!this._appConfig) {
|
||||
this._appConfig = readAppConfig({
|
||||
projectDir: this.getReactNativeProjectDirectory(),
|
||||
env: this.env,
|
||||
logger: this.logger,
|
||||
sdkVersion: this.metadata?.sdkVersion,
|
||||
}).exp;
|
||||
}
|
||||
return this._appConfig;
|
||||
}
|
||||
|
||||
public async runBuildPhase<T>(
|
||||
buildPhase: BuildPhase,
|
||||
phase: () => Promise<T>,
|
||||
{
|
||||
doNotMarkStart = false,
|
||||
doNotMarkEnd = false,
|
||||
}: {
|
||||
doNotMarkStart?: boolean;
|
||||
doNotMarkEnd?: boolean;
|
||||
} = {}
|
||||
): Promise<T> {
|
||||
let startTimestamp = Date.now();
|
||||
try {
|
||||
this.setBuildPhase(buildPhase, { doNotMarkStart });
|
||||
startTimestamp = Date.now();
|
||||
const result = await phase();
|
||||
const durationMs = Date.now() - startTimestamp;
|
||||
const buildPhaseResult: BuildPhaseResult = this.buildPhaseSkipped
|
||||
? BuildPhaseResult.SKIPPED
|
||||
: this.buildPhaseHasWarnings
|
||||
? BuildPhaseResult.WARNING
|
||||
: BuildPhaseResult.SUCCESS;
|
||||
await this.endCurrentBuildPhaseAsync({ result: buildPhaseResult, doNotMarkEnd, durationMs });
|
||||
return result;
|
||||
} catch (err: any) {
|
||||
const durationMs = Date.now() - startTimestamp;
|
||||
const resolvedError = await this.handleBuildPhaseErrorAsync(err, buildPhase);
|
||||
await this.endCurrentBuildPhaseAsync({ result: BuildPhaseResult.FAIL, durationMs });
|
||||
throw resolvedError;
|
||||
}
|
||||
}
|
||||
|
||||
public markBuildPhaseSkipped(): void {
|
||||
this.buildPhaseSkipped = true;
|
||||
}
|
||||
|
||||
public markBuildPhaseHasWarnings(): void {
|
||||
this.buildPhaseHasWarnings = true;
|
||||
}
|
||||
|
||||
public async uploadArtifact({
|
||||
artifact,
|
||||
logger,
|
||||
}: {
|
||||
artifact: ArtifactToUpload;
|
||||
logger: bunyan;
|
||||
}): Promise<{ artifactId: string | null }> {
|
||||
const result = await this._uploadArtifact({ artifact, logger });
|
||||
if (result.filename && !isGenericArtifact(artifact)) {
|
||||
this.artifacts[artifact.type] = result.filename;
|
||||
}
|
||||
return { artifactId: result.artifactId ?? null };
|
||||
}
|
||||
|
||||
public updateEnv(env: Env): void {
|
||||
if (this._job.triggeredBy !== BuildTrigger.GIT_BASED_INTEGRATION) {
|
||||
throw new Error(
|
||||
'Updating environment variables is only allowed when build was triggered by a git-based integration.'
|
||||
);
|
||||
}
|
||||
this._env = {
|
||||
...env,
|
||||
...this._env,
|
||||
__EAS_BUILD_ENVS_DIR: this.buildEnvsDirectory,
|
||||
};
|
||||
this._env.PATH = this._env.PATH
|
||||
? [this.buildExecutablesDirectory, this._env.PATH].join(':')
|
||||
: this.buildExecutablesDirectory;
|
||||
}
|
||||
|
||||
public updateJobInformation(job: TJob, metadata: Metadata): void {
|
||||
if (this._job.triggeredBy !== BuildTrigger.GIT_BASED_INTEGRATION) {
|
||||
throw new Error(
|
||||
'Updating job information is only allowed when build was triggered by a git-based integration.'
|
||||
);
|
||||
}
|
||||
this._job = {
|
||||
...this._job,
|
||||
...job,
|
||||
workflowInterpolationContext:
|
||||
job.workflowInterpolationContext ?? this.job.workflowInterpolationContext,
|
||||
triggeredBy: this._job.triggeredBy,
|
||||
secrets: {
|
||||
...this.job.secrets,
|
||||
...job.secrets,
|
||||
robotAccessToken: job.secrets?.robotAccessToken ?? this.job.secrets?.robotAccessToken,
|
||||
environmentSecrets: [
|
||||
// Latter secrets override former ones.
|
||||
...(this.job.secrets?.environmentSecrets ?? []),
|
||||
...(job.secrets?.environmentSecrets ?? []),
|
||||
],
|
||||
},
|
||||
...(this._job.platform ? { expoBuildUrl: this._job.expoBuildUrl } : null),
|
||||
};
|
||||
this._metadata = metadata;
|
||||
}
|
||||
|
||||
private async handleBuildPhaseErrorAsync(
|
||||
err: any,
|
||||
buildPhase: BuildPhase
|
||||
): Promise<errors.BuildError> {
|
||||
const buildError = await resolveBuildPhaseErrorAsync(
|
||||
err,
|
||||
this.logBuffer.getPhaseLogs(buildPhase),
|
||||
{
|
||||
job: this.job,
|
||||
phase: buildPhase,
|
||||
env: this.env,
|
||||
},
|
||||
this.buildLogsDirectory
|
||||
);
|
||||
if (buildError.errorCode === errors.ErrorCode.UNKNOWN_ERROR) {
|
||||
// leaving message empty, website will display err.stack which already includes err.message
|
||||
this.logger.error({ err }, '');
|
||||
} else {
|
||||
this.logger.error(`Error: ${buildError.userFacingMessage}`);
|
||||
}
|
||||
return buildError;
|
||||
}
|
||||
|
||||
public getReactNativeProjectDirectory(baseDirectory = this.buildDirectory): string {
|
||||
if (!this.job.platform) {
|
||||
return path.join(
|
||||
baseDirectory,
|
||||
// NOTE: We may want to add projectRootDirectory to generic jobs in the future.
|
||||
this.job.builderEnvironment.env.__EXPO_RELATIVE_BASE_DIRECTORY || '.'
|
||||
);
|
||||
}
|
||||
|
||||
return path.join(baseDirectory, this.job.projectRootDirectory ?? '.');
|
||||
}
|
||||
|
||||
private setBuildPhase(buildPhase: BuildPhase, { doNotMarkStart = false } = {}): void {
|
||||
if (this.buildPhase) {
|
||||
if (this.buildPhase === buildPhase) {
|
||||
return;
|
||||
} else {
|
||||
this.logger.info(
|
||||
{ marker: LogMarker.END_PHASE, result: BuildPhaseResult.UNKNOWN },
|
||||
`End phase: ${this.buildPhase}`
|
||||
);
|
||||
this.logger = this.defaultLogger;
|
||||
}
|
||||
}
|
||||
this.buildPhase = buildPhase;
|
||||
this.logger = this.defaultLogger.child({ phase: buildPhase });
|
||||
if (!doNotMarkStart) {
|
||||
this.logger.info({ marker: LogMarker.START_PHASE }, `Start phase: ${this.buildPhase}`);
|
||||
}
|
||||
}
|
||||
|
||||
private async endCurrentBuildPhaseAsync({
|
||||
result,
|
||||
doNotMarkEnd = false,
|
||||
durationMs,
|
||||
}: {
|
||||
result: BuildPhaseResult;
|
||||
doNotMarkEnd?: boolean;
|
||||
durationMs: number;
|
||||
}): Promise<void> {
|
||||
if (!this.buildPhase) {
|
||||
return;
|
||||
}
|
||||
await this.collectAndUpdateEnvVariablesAsync();
|
||||
|
||||
this.reportBuildPhaseStats?.({ buildPhase: this.buildPhase, result, durationMs });
|
||||
|
||||
if (!doNotMarkEnd) {
|
||||
this.logger.info(
|
||||
{ marker: LogMarker.END_PHASE, result, durationMs },
|
||||
`End phase: ${this.buildPhase}`
|
||||
);
|
||||
}
|
||||
this.logger = this.defaultLogger;
|
||||
this.buildPhase = undefined;
|
||||
this.buildPhaseSkipped = false;
|
||||
this.buildPhaseHasWarnings = false;
|
||||
}
|
||||
|
||||
private async collectAndUpdateEnvVariablesAsync(): Promise<void> {
|
||||
const filenames = await fs.readdir(this.buildEnvsDirectory);
|
||||
|
||||
const entries = await Promise.all(
|
||||
filenames.map(async basename => {
|
||||
const rawContents = await fs.readFile(
|
||||
path.join(this.buildEnvsDirectory, basename),
|
||||
'utf-8'
|
||||
);
|
||||
return [basename, rawContents];
|
||||
})
|
||||
);
|
||||
await Promise.all(
|
||||
filenames.map(async basename => {
|
||||
await fs.remove(path.join(this.buildEnvsDirectory, basename));
|
||||
})
|
||||
);
|
||||
this._env = {
|
||||
...this._env,
|
||||
...Object.fromEntries(entries),
|
||||
};
|
||||
}
|
||||
|
||||
private getEnvironmentSecrets(job: TJob): Record<string, string> {
|
||||
if (!job?.secrets?.environmentSecrets) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const environmentSecretsDirectory = path.join(this.workingdir, 'eas-environment-secrets');
|
||||
|
||||
const environmentSecrets: Record<string, string> = {};
|
||||
for (const { name, type, value } of job.secrets.environmentSecrets) {
|
||||
if (type === EnvironmentSecretType.STRING) {
|
||||
environmentSecrets[name] = value;
|
||||
} else {
|
||||
environmentSecrets[name] = createTemporaryEnvironmentSecretFile({
|
||||
secretsDir: environmentSecretsDirectory,
|
||||
name,
|
||||
contents_base64: value,
|
||||
});
|
||||
}
|
||||
}
|
||||
return environmentSecrets;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
import {
|
||||
BuildJob,
|
||||
BuildPhase,
|
||||
BuildTrigger,
|
||||
Env,
|
||||
Job,
|
||||
Metadata,
|
||||
Platform,
|
||||
StaticJobInterpolationContext,
|
||||
} from '@expo/eas-build-job';
|
||||
import { bunyan } from '@expo/logger';
|
||||
import { BuildRuntimePlatform, ExternalBuildContextProvider } from '@expo/steps';
|
||||
import assert from 'assert';
|
||||
import path from 'path';
|
||||
|
||||
import { ArtifactToUpload, BuildContext } from './context';
|
||||
|
||||
const platformToBuildRuntimePlatform: Record<Platform, BuildRuntimePlatform> = {
|
||||
[Platform.ANDROID]: BuildRuntimePlatform.LINUX,
|
||||
[Platform.IOS]: BuildRuntimePlatform.DARWIN,
|
||||
};
|
||||
|
||||
export interface BuilderRuntimeApi {
|
||||
uploadArtifact: (spec: { artifact: ArtifactToUpload; logger: bunyan }) => Promise<{
|
||||
artifactId: string | null;
|
||||
}>;
|
||||
}
|
||||
|
||||
export class CustomBuildContext<TJob extends Job = Job> implements ExternalBuildContextProvider {
|
||||
/*
|
||||
* Directory that contains project sources before eas/checkout.
|
||||
*/
|
||||
public readonly projectSourceDirectory: string;
|
||||
|
||||
/*
|
||||
* Directory where build is executed. eas/checkout will copy sources here.
|
||||
*/
|
||||
public readonly projectTargetDirectory: string;
|
||||
|
||||
/*
|
||||
* Directory where all build steps will be executed unless configured otherwise.
|
||||
*/
|
||||
public readonly defaultWorkingDirectory: string;
|
||||
|
||||
/*
|
||||
* Directory where build logs will be stored unless configure otherwise.
|
||||
*/
|
||||
public readonly buildLogsDirectory: string;
|
||||
|
||||
/**
|
||||
* Time of the start of the build.
|
||||
*/
|
||||
public readonly startTime: Date;
|
||||
|
||||
public readonly logger: bunyan;
|
||||
public readonly runtimeApi: BuilderRuntimeApi;
|
||||
public job: TJob;
|
||||
public metadata?: Metadata;
|
||||
|
||||
private _env: Env;
|
||||
|
||||
constructor(buildCtx: BuildContext<TJob>) {
|
||||
this._env = buildCtx.env;
|
||||
this.job = buildCtx.job;
|
||||
this.metadata = buildCtx.metadata;
|
||||
|
||||
this.logger = buildCtx.logger.child({ phase: BuildPhase.CUSTOM });
|
||||
this.projectSourceDirectory = path.join(buildCtx.workingdir, 'temporary-custom-build');
|
||||
this.projectTargetDirectory = path.join(buildCtx.workingdir, 'build');
|
||||
this.defaultWorkingDirectory = buildCtx.getReactNativeProjectDirectory();
|
||||
this.buildLogsDirectory = path.join(buildCtx.workingdir, 'logs');
|
||||
this.runtimeApi = {
|
||||
uploadArtifact: (...args) => buildCtx['uploadArtifact'](...args),
|
||||
};
|
||||
this.startTime = new Date();
|
||||
}
|
||||
|
||||
public hasBuildJob(): this is CustomBuildContext<BuildJob> {
|
||||
return Boolean(this.job.platform);
|
||||
}
|
||||
|
||||
public get runtimePlatform(): BuildRuntimePlatform {
|
||||
// Generic jobs are not per-platform.
|
||||
if (!this.job.platform) {
|
||||
assert(
|
||||
process.platform === 'linux' || process.platform === 'darwin',
|
||||
`Invalid platform, expected linux or darwin, got: ${process.platform}`
|
||||
);
|
||||
return {
|
||||
linux: BuildRuntimePlatform.LINUX,
|
||||
darwin: BuildRuntimePlatform.DARWIN,
|
||||
}[process.platform];
|
||||
}
|
||||
|
||||
return platformToBuildRuntimePlatform[this.job.platform];
|
||||
}
|
||||
|
||||
public get env(): Env {
|
||||
return this._env;
|
||||
}
|
||||
|
||||
// We omit steps, because CustomBuildContext does not have steps.
|
||||
public staticContext(): Omit<StaticJobInterpolationContext, 'steps'> {
|
||||
return {
|
||||
...this.job.workflowInterpolationContext,
|
||||
expoApiServerURL: this.env.__API_SERVER_URL,
|
||||
job: this.job,
|
||||
metadata: this.metadata ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
public updateEnv(env: Env): void {
|
||||
this._env = env;
|
||||
}
|
||||
|
||||
public updateJobInformation(job: TJob, metadata: Metadata): void {
|
||||
if (this.job.triggeredBy !== BuildTrigger.GIT_BASED_INTEGRATION) {
|
||||
throw new Error(
|
||||
'Updating job information is only allowed when build was triggered by a git-based integration.'
|
||||
);
|
||||
}
|
||||
this.job = {
|
||||
...this.job,
|
||||
...job,
|
||||
workflowInterpolationContext:
|
||||
job.workflowInterpolationContext ?? this.job.workflowInterpolationContext,
|
||||
triggeredBy: this.job.triggeredBy,
|
||||
secrets: {
|
||||
...this.job.secrets,
|
||||
...job.secrets,
|
||||
robotAccessToken: job.secrets?.robotAccessToken ?? this.job.secrets?.robotAccessToken,
|
||||
environmentSecrets: [
|
||||
// Latter secrets override former ones.
|
||||
...(this.job.secrets?.environmentSecrets ?? []),
|
||||
...(job.secrets?.environmentSecrets ?? []),
|
||||
],
|
||||
},
|
||||
...(this.job.platform ? { expoBuildUrl: this.job.expoBuildUrl } : null),
|
||||
};
|
||||
this.metadata = metadata;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,237 @@
|
||||
import { bunyan } from '@expo/logger';
|
||||
import assert from 'assert';
|
||||
import { randomUUID } from 'crypto';
|
||||
import fs from 'fs-extra';
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
import { Readable, Writable, pipeline } from 'stream';
|
||||
import { promisify } from 'util';
|
||||
import zlib from 'zlib';
|
||||
|
||||
import GCS from './client';
|
||||
|
||||
type PromiseResolveFn = (value?: void | PromiseLike<void> | undefined) => void;
|
||||
|
||||
const pipe = promisify(pipeline);
|
||||
|
||||
class GCSLoggerStream extends Writable {
|
||||
public writable = true;
|
||||
private readonly logger: bunyan;
|
||||
private readonly uploadMethod?: GCSLoggerStream.UploadMethod;
|
||||
private readonly options: GCSLoggerStream.Options;
|
||||
private readonly temporaryLogsPath: string;
|
||||
private readonly temporaryCompressedLogsPath: string;
|
||||
private readonly compress: string | null;
|
||||
private fileHandle?: number;
|
||||
private uploadingPromise?: Promise<void>;
|
||||
private hasChangesToUpload = false;
|
||||
private flushInterval?: NodeJS.Timeout;
|
||||
private buffer: string[] = [];
|
||||
private writePromise?: Promise<any>;
|
||||
private cleanUpCalled: boolean = false;
|
||||
|
||||
constructor({ logger, uploadMethod, options }: GCSLoggerStream.Config) {
|
||||
super();
|
||||
this.logger = logger;
|
||||
this.uploadMethod = uploadMethod;
|
||||
this.options = options;
|
||||
this.compress = options?.compress ?? this.findNormalizedHeader('contentencoding');
|
||||
this.temporaryLogsPath = path.join(os.tmpdir(), `logs-${randomUUID()}`);
|
||||
this.temporaryCompressedLogsPath = `${this.temporaryLogsPath}.compressed`;
|
||||
}
|
||||
|
||||
private findNormalizedHeader(name: string): string | null {
|
||||
if (!this.uploadMethod || 'client' in this.uploadMethod) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const normalizedName = name.toLowerCase().replace('-', '');
|
||||
const fields = this.uploadMethod.signedUrl.headers;
|
||||
for (const key in fields) {
|
||||
if (key.toLowerCase().replace('-', '') === normalizedName) {
|
||||
return fields[key];
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public async init(): Promise<string> {
|
||||
this.fileHandle = await fs.open(this.temporaryLogsPath, 'w+', 0o660);
|
||||
this.flushInterval = setInterval(() => this.flush(), this.options.uploadIntervalMs);
|
||||
return (await this.flush(true)) as string;
|
||||
}
|
||||
|
||||
public async cleanUp(): Promise<void> {
|
||||
if (this.cleanUpCalled) {
|
||||
this.logger.info('Cleanup already called');
|
||||
return;
|
||||
}
|
||||
this.cleanUpCalled = true;
|
||||
if (!this.fileHandle) {
|
||||
throw new Error('You have to init the stream first!');
|
||||
}
|
||||
if (this.flushInterval) {
|
||||
clearInterval(this.flushInterval);
|
||||
this.flushInterval = undefined;
|
||||
}
|
||||
|
||||
await this.safeWriteToFile(true);
|
||||
await fs.close(this.fileHandle);
|
||||
this.fileHandle = undefined;
|
||||
|
||||
await this.flush(true);
|
||||
|
||||
await fs.remove(this.temporaryLogsPath);
|
||||
await fs.remove(this.temporaryCompressedLogsPath);
|
||||
this.logger.info('Cleaning up GCS log stream');
|
||||
}
|
||||
|
||||
public write(rec: any): boolean {
|
||||
if (!this.fileHandle) {
|
||||
return true;
|
||||
}
|
||||
const logLine = `${JSON.stringify(rec)}\n`;
|
||||
this.buffer.push(logLine);
|
||||
void this.safeWriteToFile();
|
||||
return true;
|
||||
}
|
||||
|
||||
private async safeWriteToFile(force = false): Promise<void> {
|
||||
if (this.writePromise && force) {
|
||||
await this.writePromise;
|
||||
await this.safeWriteToFile(force);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.fileHandle || Boolean(this.writePromise) || this.buffer.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const buffer = this.buffer.slice();
|
||||
try {
|
||||
this.writePromise = fs.write(this.fileHandle, buffer.join(''));
|
||||
await this.writePromise;
|
||||
this.buffer = this.buffer.slice(buffer.length);
|
||||
this.hasChangesToUpload = true;
|
||||
} catch (err) {
|
||||
this.logger.error({ err, origin: 'gcs-logger' }, 'Failed to write logs to file');
|
||||
} finally {
|
||||
this.writePromise = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private async flush(force = false): Promise<string | void> {
|
||||
if (force || this.hasChangesToUpload) {
|
||||
if (this.uploadingPromise) {
|
||||
await this.uploadingPromise;
|
||||
return await this.flush(force);
|
||||
}
|
||||
return await this.flushInternal();
|
||||
}
|
||||
}
|
||||
|
||||
private async flushInternal(): Promise<string | void> {
|
||||
await this.safeWriteToFile();
|
||||
|
||||
let resolveFn: PromiseResolveFn;
|
||||
this.hasChangesToUpload = false;
|
||||
this.uploadingPromise = new Promise(res => {
|
||||
resolveFn = res;
|
||||
});
|
||||
return await this.upload()
|
||||
.then(result => {
|
||||
return result;
|
||||
})
|
||||
.catch(err => {
|
||||
this.logger.error({ err }, 'Failed to upload logs file to GCS');
|
||||
})
|
||||
.then(result => {
|
||||
this.uploadingPromise = undefined;
|
||||
resolveFn();
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
private async upload(): Promise<string | void> {
|
||||
if (!this.uploadMethod) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { size } = await fs.stat(this.temporaryLogsPath);
|
||||
const srcGeneratorAsync = async (): Promise<Readable> => {
|
||||
return await this.createCompressedStream(
|
||||
fs.createReadStream(this.temporaryLogsPath, { end: size })
|
||||
);
|
||||
};
|
||||
|
||||
if ('signedUrl' in this.uploadMethod) {
|
||||
return await GCS.uploadWithSignedUrl({
|
||||
signedUrl: this.uploadMethod.signedUrl,
|
||||
srcGeneratorAsync,
|
||||
});
|
||||
}
|
||||
|
||||
const { Location } = await this.uploadMethod.client.uploadFile({
|
||||
key: this.uploadMethod.key,
|
||||
src: await srcGeneratorAsync(),
|
||||
streamOptions: {
|
||||
metadata: {
|
||||
contentType: 'text/plain;charset=utf-8',
|
||||
...(this.compress !== null ? { contentEncoding: this.compress } : {}),
|
||||
customTime: this.uploadMethod.customTime,
|
||||
},
|
||||
},
|
||||
});
|
||||
return Location;
|
||||
}
|
||||
|
||||
private async createCompressedStream(src: Readable): Promise<Readable> {
|
||||
if (!this.compress) {
|
||||
return src;
|
||||
}
|
||||
|
||||
const dst = fs.createWriteStream(this.temporaryCompressedLogsPath);
|
||||
|
||||
const encoder =
|
||||
this.compress === 'gzip'
|
||||
? zlib.createGzip()
|
||||
: this.compress === 'br'
|
||||
? zlib.createBrotliCompress({
|
||||
params: {
|
||||
[zlib.constants.BROTLI_PARAM_QUALITY]: 0,
|
||||
},
|
||||
})
|
||||
: null;
|
||||
|
||||
assert(encoder, `unknown encoder ${encoder}`);
|
||||
await pipe(src, encoder, dst);
|
||||
|
||||
const { size } = await fs.stat(this.temporaryCompressedLogsPath);
|
||||
return fs.createReadStream(this.temporaryCompressedLogsPath, { end: size });
|
||||
}
|
||||
}
|
||||
|
||||
namespace GCSLoggerStream {
|
||||
export enum CompressionMethod {
|
||||
GZIP = 'gzip',
|
||||
BR = 'br',
|
||||
}
|
||||
|
||||
export interface Options {
|
||||
uploadIntervalMs: number;
|
||||
compress?: CompressionMethod;
|
||||
}
|
||||
|
||||
export type UploadMethod =
|
||||
| { client: GCS; key: string; customTime: Date | null }
|
||||
| { signedUrl: GCS.SignedUrl };
|
||||
|
||||
export interface Config {
|
||||
logger: bunyan;
|
||||
uploadMethod?: UploadMethod;
|
||||
options: Options;
|
||||
}
|
||||
}
|
||||
|
||||
export default GCSLoggerStream;
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.9 KiB |
@@ -0,0 +1,383 @@
|
||||
import { randomBytes } from 'crypto';
|
||||
import fs from 'fs-extra';
|
||||
import fetch, { RequestInit, Response } from 'node-fetch';
|
||||
import path from 'path';
|
||||
import { Readable } from 'stream';
|
||||
|
||||
import GCS from '../client';
|
||||
|
||||
jest.mock('node-fetch');
|
||||
|
||||
class ErrorWithCode extends Error {
|
||||
protected readonly _code: string | undefined;
|
||||
|
||||
constructor(message: string, code?: string) {
|
||||
super(message);
|
||||
if (code) {
|
||||
this._code = code;
|
||||
}
|
||||
}
|
||||
|
||||
public get code(): string | undefined {
|
||||
return this._code;
|
||||
}
|
||||
}
|
||||
|
||||
class DNSError extends ErrorWithCode {
|
||||
protected readonly _code: 'ENOTFOUND' | 'EAI_AGAIN' = 'ENOTFOUND';
|
||||
}
|
||||
|
||||
const TEST_BUCKET = 'turtle-v2-test';
|
||||
|
||||
let googleApplicationCredentials: string | undefined;
|
||||
beforeAll(() => {
|
||||
googleApplicationCredentials = process.env.GOOGLE_APPLICATION_CREDENTIALS;
|
||||
process.env.GOOGLE_APPLICATION_CREDENTIALS = path.join(__dirname, 'mock-credentials.json');
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
process.env.GOOGLE_APPLICATION_CREDENTIALS = googleApplicationCredentials;
|
||||
});
|
||||
|
||||
describe('GCS client', () => {
|
||||
describe('uploadWithPresignedURL function', () => {
|
||||
it('should throw an error if upload fails', async () => {
|
||||
const fetchMock = jest.mocked(fetch);
|
||||
const res = {
|
||||
ok: false,
|
||||
status: 500,
|
||||
} as Response;
|
||||
|
||||
const localImagePath = path.join(__dirname, 'cat.jpg');
|
||||
const key = 'cat.jpg';
|
||||
|
||||
const gcs = new GCS(TEST_BUCKET);
|
||||
const signedUrl = await gcs.createSignedUploadUrl({
|
||||
key,
|
||||
expirationTime: 30000,
|
||||
contentType: 'image/jpeg',
|
||||
});
|
||||
|
||||
fetchMock.mockImplementation(async () => res);
|
||||
await expect(
|
||||
GCS.uploadWithSignedUrl({
|
||||
signedUrl,
|
||||
srcGeneratorAsync: async () => fs.createReadStream(localImagePath),
|
||||
retryIntervalMs: 500,
|
||||
})
|
||||
).rejects.toThrow();
|
||||
});
|
||||
|
||||
it('should return stripped URL if successful', async () => {
|
||||
const fetchMock = jest.mocked(fetch);
|
||||
const res = {
|
||||
ok: true,
|
||||
status: 200,
|
||||
} as Response;
|
||||
|
||||
const localImagePath = path.join(__dirname, 'cat.jpg');
|
||||
const key = 'cat.jpg';
|
||||
|
||||
const gcs = new GCS(TEST_BUCKET);
|
||||
const signedUrl = await gcs.createSignedUploadUrl({
|
||||
key,
|
||||
expirationTime: 30000,
|
||||
contentType: 'image/jpeg',
|
||||
});
|
||||
|
||||
fetchMock.mockImplementation(async () => res);
|
||||
const result = await GCS.uploadWithSignedUrl({
|
||||
signedUrl,
|
||||
srcGeneratorAsync: async () => fs.createReadStream(localImagePath),
|
||||
retryIntervalMs: 500,
|
||||
});
|
||||
expect(result).toEqual('https://storage.googleapis.com/turtle-v2-test/cat.jpg');
|
||||
});
|
||||
|
||||
it('should retry upload on DNS error up to 2 times', async () => {
|
||||
const fetchMock = jest.mocked(fetch);
|
||||
const res = {
|
||||
ok: true,
|
||||
status: 200,
|
||||
} as Response;
|
||||
|
||||
const localImagePath = path.join(__dirname, 'cat.jpg');
|
||||
const key = 'cat.jpg';
|
||||
|
||||
const gcs = new GCS(TEST_BUCKET);
|
||||
const signedUrl = await gcs.createSignedUploadUrl({
|
||||
key,
|
||||
expirationTime: 30000,
|
||||
contentType: 'image/jpeg',
|
||||
});
|
||||
|
||||
fetchMock
|
||||
.mockImplementationOnce(async () => {
|
||||
throw new DNSError('failed once');
|
||||
})
|
||||
.mockImplementationOnce(async () => {
|
||||
throw new DNSError('failed twice', 'EAI_AGAIN');
|
||||
})
|
||||
.mockImplementation(async () => res);
|
||||
const result = await GCS.uploadWithSignedUrl({
|
||||
signedUrl,
|
||||
srcGeneratorAsync: async () => fs.createReadStream(localImagePath),
|
||||
retryIntervalMs: 500,
|
||||
});
|
||||
expect(result).toEqual('https://storage.googleapis.com/turtle-v2-test/cat.jpg');
|
||||
expect(fetchMock).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
|
||||
it('should retry upload on retriable status codes error up to 2 times', async () => {
|
||||
const fetchMock = jest.mocked(fetch);
|
||||
const res = {
|
||||
ok: true,
|
||||
status: 200,
|
||||
} as Response;
|
||||
|
||||
const localImagePath = path.join(__dirname, 'cat.jpg');
|
||||
const key = 'cat.jpg';
|
||||
|
||||
const gcs = new GCS(TEST_BUCKET);
|
||||
const signedUrl = await gcs.createSignedUploadUrl({
|
||||
key,
|
||||
expirationTime: 30000,
|
||||
contentType: 'image/jpeg',
|
||||
});
|
||||
|
||||
fetchMock
|
||||
.mockImplementationOnce(async () => {
|
||||
return {
|
||||
ok: false,
|
||||
status: 503,
|
||||
} as Response;
|
||||
})
|
||||
.mockImplementationOnce(async () => {
|
||||
return {
|
||||
ok: false,
|
||||
status: 408,
|
||||
} as Response;
|
||||
})
|
||||
.mockImplementation(async () => res);
|
||||
const result = await GCS.uploadWithSignedUrl({
|
||||
signedUrl,
|
||||
srcGeneratorAsync: async () => fs.createReadStream(localImagePath),
|
||||
retryIntervalMs: 500,
|
||||
});
|
||||
expect(result).toEqual('https://storage.googleapis.com/turtle-v2-test/cat.jpg');
|
||||
expect(fetchMock).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
|
||||
it('should retry upload on retriable status codes and DNS errors error up to 2 times', async () => {
|
||||
const fetchMock = jest.mocked(fetch);
|
||||
const res = {
|
||||
ok: true,
|
||||
status: 200,
|
||||
} as Response;
|
||||
|
||||
const localImagePath = path.join(__dirname, 'cat.jpg');
|
||||
const key = 'cat.jpg';
|
||||
|
||||
const gcs = new GCS(TEST_BUCKET);
|
||||
const signedUrl = await gcs.createSignedUploadUrl({
|
||||
key,
|
||||
expirationTime: 30000,
|
||||
contentType: 'image/jpeg',
|
||||
});
|
||||
|
||||
fetchMock
|
||||
.mockImplementationOnce(async () => {
|
||||
return {
|
||||
ok: false,
|
||||
status: 503,
|
||||
} as Response;
|
||||
})
|
||||
.mockImplementationOnce(async () => {
|
||||
throw new DNSError('failed once');
|
||||
})
|
||||
.mockImplementation(async () => res);
|
||||
const result = await GCS.uploadWithSignedUrl({
|
||||
signedUrl,
|
||||
srcGeneratorAsync: async () => fs.createReadStream(localImagePath),
|
||||
retryIntervalMs: 500,
|
||||
});
|
||||
expect(result).toEqual('https://storage.googleapis.com/turtle-v2-test/cat.jpg');
|
||||
expect(fetchMock).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
|
||||
it('should not retry upload on DNS error more than 2 times', async () => {
|
||||
const fetchMock = jest.mocked(fetch);
|
||||
const res = {
|
||||
ok: true,
|
||||
status: 200,
|
||||
} as Response;
|
||||
|
||||
const localImagePath = path.join(__dirname, 'cat.jpg');
|
||||
const key = 'cat.jpg';
|
||||
|
||||
const gcs = new GCS(TEST_BUCKET);
|
||||
const signedUrl = await gcs.createSignedUploadUrl({
|
||||
key,
|
||||
expirationTime: 30000,
|
||||
contentType: 'image/jpeg',
|
||||
});
|
||||
|
||||
const lastDNSError = new DNSError('failed thrice');
|
||||
fetchMock
|
||||
.mockImplementationOnce(async () => {
|
||||
throw new DNSError('failed once');
|
||||
})
|
||||
.mockImplementationOnce(async () => {
|
||||
throw new DNSError('failed twice', 'EAI_AGAIN');
|
||||
})
|
||||
.mockImplementationOnce(async () => {
|
||||
throw lastDNSError;
|
||||
})
|
||||
.mockImplementation(async () => res);
|
||||
await expect(
|
||||
GCS.uploadWithSignedUrl({
|
||||
signedUrl,
|
||||
srcGeneratorAsync: async () => fs.createReadStream(localImagePath),
|
||||
retryIntervalMs: 500,
|
||||
})
|
||||
).rejects.toThrow(lastDNSError);
|
||||
expect(fetchMock).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
|
||||
it('should not retry upload on retriable status code more than 2 times', async () => {
|
||||
const fetchMock = jest.mocked(fetch);
|
||||
const res = {
|
||||
ok: true,
|
||||
status: 200,
|
||||
} as Response;
|
||||
|
||||
const localImagePath = path.join(__dirname, 'cat.jpg');
|
||||
const key = 'cat.jpg';
|
||||
|
||||
const gcs = new GCS(TEST_BUCKET);
|
||||
const signedUrl = await gcs.createSignedUploadUrl({
|
||||
key,
|
||||
expirationTime: 30000,
|
||||
contentType: 'image/jpeg',
|
||||
});
|
||||
|
||||
fetchMock
|
||||
.mockImplementationOnce(async () => {
|
||||
return {
|
||||
ok: false,
|
||||
status: 503,
|
||||
} as Response;
|
||||
})
|
||||
.mockImplementationOnce(async () => {
|
||||
return {
|
||||
ok: false,
|
||||
status: 408,
|
||||
} as Response;
|
||||
})
|
||||
.mockImplementationOnce(async () => {
|
||||
return {
|
||||
ok: false,
|
||||
status: 504,
|
||||
} as Response;
|
||||
})
|
||||
.mockImplementation(async () => res);
|
||||
await expect(
|
||||
GCS.uploadWithSignedUrl({
|
||||
signedUrl,
|
||||
srcGeneratorAsync: async () => fs.createReadStream(localImagePath),
|
||||
retryIntervalMs: 500,
|
||||
})
|
||||
).rejects.toThrow();
|
||||
expect(fetchMock).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
|
||||
it('should not retry upload on other error codes', async () => {
|
||||
const fetchMock = jest.mocked(fetch);
|
||||
const res = {
|
||||
ok: true,
|
||||
status: 200,
|
||||
} as Response;
|
||||
|
||||
const localImagePath = path.join(__dirname, 'cat.jpg');
|
||||
const key = 'cat.jpg';
|
||||
|
||||
const gcs = new GCS(TEST_BUCKET);
|
||||
const signedUrl = await gcs.createSignedUploadUrl({
|
||||
key,
|
||||
expirationTime: 30000,
|
||||
contentType: 'image/jpeg',
|
||||
});
|
||||
|
||||
const nonDNSError = new ErrorWithCode('failed once', 'A_DIFFERENT_CODE');
|
||||
fetchMock
|
||||
.mockImplementationOnce(async () => {
|
||||
throw nonDNSError;
|
||||
})
|
||||
.mockImplementation(async () => res);
|
||||
await expect(
|
||||
GCS.uploadWithSignedUrl({
|
||||
signedUrl,
|
||||
srcGeneratorAsync: async () => fs.createReadStream(localImagePath),
|
||||
retryIntervalMs: 500,
|
||||
})
|
||||
).rejects.toThrow(nonDNSError);
|
||||
expect(fetchMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('retries upload with full stream', async () => {
|
||||
const fetchMock = jest.mocked(fetch);
|
||||
const receivedBodies: Buffer[] = [];
|
||||
const recordBody = async (request: RequestInit | undefined): Promise<void> => {
|
||||
let body = Buffer.from([]);
|
||||
for await (const chunk of request!.body as Readable) {
|
||||
body = Buffer.concat([body, chunk]);
|
||||
}
|
||||
receivedBodies.push(body);
|
||||
};
|
||||
fetchMock
|
||||
.mockImplementationOnce(async (_path, request) => {
|
||||
await recordBody(request);
|
||||
return {
|
||||
ok: false,
|
||||
status: 503,
|
||||
} as Response;
|
||||
})
|
||||
.mockImplementationOnce(async (_path, request) => {
|
||||
await recordBody(request);
|
||||
throw new DNSError('failed once');
|
||||
})
|
||||
.mockImplementation(async (_path, request) => {
|
||||
await recordBody(request);
|
||||
return {
|
||||
ok: true,
|
||||
status: 201,
|
||||
} as Response;
|
||||
});
|
||||
|
||||
const gcs = new GCS(TEST_BUCKET);
|
||||
const signedUrl = await gcs.createSignedUploadUrl({
|
||||
key: 'text.txt',
|
||||
expirationTime: 30000,
|
||||
contentType: 'text/plain',
|
||||
});
|
||||
|
||||
const bufferToUpload = randomBytes(16);
|
||||
|
||||
const result = await GCS.uploadWithSignedUrl({
|
||||
signedUrl,
|
||||
srcGeneratorAsync: async () => Readable.from(bufferToUpload),
|
||||
retryIntervalMs: 500,
|
||||
});
|
||||
expect(result).toEqual('https://storage.googleapis.com/turtle-v2-test/text.txt');
|
||||
expect(fetchMock).toHaveBeenCalledTimes(3);
|
||||
expect(receivedBodies.length).toBe(3);
|
||||
for (const body of receivedBodies) {
|
||||
// Here we're testing that each body received in every retry is the same.
|
||||
// If we passed the same body instance to each of the retries
|
||||
// each retry would consume a chunk of the same stream,
|
||||
// causing the uploaded file to be corrupted (missing first bytes).
|
||||
expect(body).toStrictEqual(bufferToUpload);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"type": "service_account",
|
||||
"project_id": "mock-project",
|
||||
"private_key_id": "mock-private-key-id",
|
||||
"private_key": "-----BEGIN RSA PRIVATE KEY-----\nMIICXQIBAAKBgQCMk5Qbd5E8RQeP+2vObJVJVnXWXfW/OStxkAYe7r+KVXzHyLYU\nD6Ejt94OiC3X2+tqrMIZgNAUR3VF5tSiP57Il0iWHSspmNIHz7cW6/FHiQsvN2oJ\n8Fzz03vK/FfaU50QWO+BwRnixJ0xG1kSX3MxNyRZ5omGJgcwx0l6Jt878QIDAQAB\nAoGAcGZbqO6ceNU068H6/A1D/GSeSa3NHX2np+ChlEAPdJtP7yojs7yfekC801+9\nT/gurpe9hsjBF0XflemwIJ6/5KucWHrcp2cCkdBHtkK6ScHeIVl3DcOYYhxqJrAA\nYiyGNh1+pf3FMPFBcPJ04pQT4PqOif61/W3p8eJFUke5x7UCQQDoyJ427wrUCZb+\nj2GD5uVyxQa4OJxDbRFriK5LTdwLeFeOmQhoo6kaGywILn6UtlPTUJ38ROA4Jv58\nEJf/jssDAkEAmpi/JkWlOzSM9mnb9blt/fH/DRKu9z0JPvl47V5YAzBhUtUMK15g\nSbEl+AJZyaklpJcgv6ShiGY0PLdoY7oQ+wJBAMF6VX4lKqPYIKcN9ygRlm3Q8ufV\nLZQhKBRvsyYl3Zmu+V8tNL78IEXxhaR7OHxUGtINNHKDsLUbO/NUO3GOdo8CQQCV\nV0sIIK0+JUSq4ZYvqKI9d6Fnso28noSpBfuwabvh0MGjb9Viq7eeWHeSPksYSMLp\nXWiwWMwGZJy/rnk0JVEzAkADiiARp70BZgRB8ogBx+DGTravfhPP64eFMHJnUgAE\nmDKlj3v0x3OZfdUz6Us5hfy3WqFyYbcCGLf7joVVZAEm\n-----END RSA PRIVATE KEY-----",
|
||||
"client_email": "mock-client-email",
|
||||
"client_id": "mock-client-id",
|
||||
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
|
||||
"token_uri": "https://oauth2.googleapis.com/token",
|
||||
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
|
||||
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/mock-client-email",
|
||||
"universe_domain": "googleapis.com"
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
import { CreateWriteStreamOptions, File, Storage } from '@google-cloud/storage';
|
||||
import fs from 'fs-extra';
|
||||
import fetch, { FetchError } from 'node-fetch';
|
||||
import stream from 'node:stream';
|
||||
import { promisify } from 'node:util';
|
||||
import { Readable } from 'stream';
|
||||
import { URL } from 'url';
|
||||
|
||||
import { RetryOptions, retryOnGCSUploadFailure } from './retry';
|
||||
|
||||
const pipeline = promisify(stream.pipeline);
|
||||
|
||||
interface SignedUrlParams {
|
||||
key: string;
|
||||
expirationTime: number;
|
||||
contentType?: string;
|
||||
extensionHeaders?: { [key: string]: number | string | string[] };
|
||||
}
|
||||
|
||||
interface UploadWithSignedUrlParams {
|
||||
signedUrl: GCS.SignedUrl;
|
||||
srcGeneratorAsync: () => Promise<Readable>;
|
||||
retryIntervalMs?: RetryOptions['retryIntervalMs'];
|
||||
retries?: RetryOptions['retries'];
|
||||
}
|
||||
|
||||
class GCS {
|
||||
private readonly client = new Storage();
|
||||
|
||||
constructor(private readonly bucket: string) {
|
||||
this.bucket = bucket;
|
||||
}
|
||||
|
||||
public static async uploadWithSignedUrl({
|
||||
signedUrl,
|
||||
srcGeneratorAsync,
|
||||
retries = 2,
|
||||
retryIntervalMs = 30_000,
|
||||
}: UploadWithSignedUrlParams): Promise<string> {
|
||||
let resp;
|
||||
try {
|
||||
resp = await retryOnGCSUploadFailure(
|
||||
async () => {
|
||||
const src = await srcGeneratorAsync();
|
||||
return await fetch(signedUrl.url, {
|
||||
method: 'PUT',
|
||||
headers: signedUrl.headers,
|
||||
body: src,
|
||||
});
|
||||
},
|
||||
{
|
||||
retries,
|
||||
retryIntervalMs,
|
||||
}
|
||||
);
|
||||
} catch (err: any) {
|
||||
if (err instanceof FetchError) {
|
||||
throw new Error(`Failed to upload the file, reason: ${err.code}`);
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
if (!resp.ok) {
|
||||
let body: string | undefined;
|
||||
try {
|
||||
body = await resp.text();
|
||||
} catch {}
|
||||
throw new Error(
|
||||
`Failed to upload file: status: ${resp.status} status text: ${resp.statusText}, body: ${body}`
|
||||
);
|
||||
}
|
||||
const url = new URL(signedUrl.url);
|
||||
return `${url.protocol}//${url.host}${url.pathname}`; // strip query string
|
||||
}
|
||||
|
||||
public formatHttpUrl(key: string): string {
|
||||
return this.client.bucket(this.bucket).file(key).publicUrl();
|
||||
}
|
||||
|
||||
public async uploadFile({
|
||||
key,
|
||||
src,
|
||||
streamOptions,
|
||||
}: {
|
||||
key: string;
|
||||
src: Readable;
|
||||
streamOptions?: CreateWriteStreamOptions;
|
||||
}): Promise<{ Location: string }> {
|
||||
const file = this.client.bucket(this.bucket).file(key);
|
||||
await new Promise<void>((res, rej) => {
|
||||
src.pipe(
|
||||
file
|
||||
.createWriteStream(streamOptions)
|
||||
.on('error', err => {
|
||||
rej(err);
|
||||
})
|
||||
.on('finish', () => {
|
||||
res();
|
||||
})
|
||||
);
|
||||
});
|
||||
return { Location: file.publicUrl() };
|
||||
}
|
||||
|
||||
public async deleteFile(key: string): Promise<void> {
|
||||
try {
|
||||
await this.client.bucket(this.bucket).file(key).delete();
|
||||
} catch (err: any) {
|
||||
if (err.response?.statusCode === 404) {
|
||||
return;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
public async createSignedUploadUrl({
|
||||
key,
|
||||
expirationTime,
|
||||
contentType,
|
||||
extensionHeaders = {},
|
||||
}: SignedUrlParams): Promise<GCS.SignedUrl> {
|
||||
const config = {
|
||||
version: 'v4' as const,
|
||||
action: 'write' as const,
|
||||
expires: Date.now() + expirationTime,
|
||||
contentType,
|
||||
extensionHeaders,
|
||||
};
|
||||
|
||||
const [url] = await this.client.bucket(this.bucket).file(key).getSignedUrl(config);
|
||||
|
||||
return {
|
||||
url,
|
||||
headers: {
|
||||
...(contentType ? { 'content-type': contentType } : {}),
|
||||
...extensionHeaders,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
public async createSignedDownloadUrl({
|
||||
key,
|
||||
expirationTime,
|
||||
}: {
|
||||
key: string;
|
||||
expirationTime: number;
|
||||
}): Promise<string> {
|
||||
const options = {
|
||||
version: 'v4' as const,
|
||||
action: 'read' as const,
|
||||
expires: Date.now() + expirationTime,
|
||||
};
|
||||
|
||||
const [url] = await this.client.bucket(this.bucket).file(key).getSignedUrl(options);
|
||||
|
||||
return url;
|
||||
}
|
||||
|
||||
public async checkIfFileExists(key: string, fileHash?: string): Promise<boolean> {
|
||||
let metadata;
|
||||
try {
|
||||
[metadata] = await this.client.bucket(this.bucket).file(key).getMetadata();
|
||||
} catch (error: any) {
|
||||
if (error.code === 404) {
|
||||
return false;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
return fileHash ? metadata.etag === fileHash : true;
|
||||
}
|
||||
|
||||
public async listDirectory(prefix: string): Promise<string[]> {
|
||||
const [files] = await this.client.bucket(this.bucket).getFiles({ prefix });
|
||||
return files.map(x => this.formatHttpUrl(x.name));
|
||||
}
|
||||
|
||||
public async moveFile(src: string, dest: string): Promise<void> {
|
||||
await this.client.bucket(this.bucket).file(src).move(dest);
|
||||
}
|
||||
|
||||
public async deleteFiles(keys: string[]): Promise<void> {
|
||||
await Promise.all(keys.map(key => this.deleteFile(key)));
|
||||
}
|
||||
|
||||
public async downloadFile(key: string, destinationPath: string): Promise<void> {
|
||||
const stream = this.client.bucket(this.bucket).file(key).createReadStream();
|
||||
await pipeline(stream, fs.createWriteStream(destinationPath));
|
||||
}
|
||||
|
||||
public getFile(key: string): File {
|
||||
return this.client.bucket(this.bucket).file(key);
|
||||
}
|
||||
}
|
||||
|
||||
namespace GCS {
|
||||
export interface SignedUrl {
|
||||
url: string;
|
||||
headers: { [key: string]: string };
|
||||
}
|
||||
|
||||
export interface Config {
|
||||
accessKeyId: string;
|
||||
secretAccessKey: string;
|
||||
region: string;
|
||||
bucket: string;
|
||||
}
|
||||
}
|
||||
|
||||
export default GCS;
|
||||
@@ -0,0 +1,65 @@
|
||||
import { Response } from 'node-fetch';
|
||||
|
||||
export interface RetryOptions {
|
||||
retries: number;
|
||||
retryIntervalMs: number;
|
||||
shouldRetryOnError: (error: any) => boolean;
|
||||
shouldRetryOnResponse: (response: Response) => boolean;
|
||||
}
|
||||
|
||||
// based on https://github.com/googleapis/nodejs-storage/blob/8ab50804fc7bae3bbd159bbb4adf65c02215b11b/src/storage.ts#L284-L320
|
||||
export async function retryOnGCSUploadFailure(
|
||||
fn: (attemptCount: number) => Promise<Response>,
|
||||
{
|
||||
retries,
|
||||
retryIntervalMs,
|
||||
}: { retries: RetryOptions['retries']; retryIntervalMs: RetryOptions['retryIntervalMs'] }
|
||||
): Promise<Response> {
|
||||
return await retry(fn, {
|
||||
retries,
|
||||
retryIntervalMs,
|
||||
shouldRetryOnError: e => {
|
||||
return (
|
||||
e.code === 'ENOTFOUND' ||
|
||||
e.code === 'EAI_AGAIN' ||
|
||||
e.code === 'ECONNRESET' ||
|
||||
e.code === 'ETIMEDOUT' ||
|
||||
e.code === 'EPIPE'
|
||||
);
|
||||
},
|
||||
shouldRetryOnResponse: resp => {
|
||||
return [408, 429, 500, 502, 503, 504].includes(resp.status);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper used to execute an inner function and possibly retry it if it throws and error
|
||||
* @param fn Function to be executed and retried in case of error
|
||||
* @param retries How many times at most should the function be retried
|
||||
* @param retryIntervalMs Time interval between the retries
|
||||
* @param shouldRetryOnError Function that determines if the function should be retried based on the error thrown
|
||||
* @param shouldRetryOnResponse Function that determines if the function should be retried based on the response
|
||||
*/
|
||||
async function retry(
|
||||
fn: (attemptCount: number) => Promise<Response>,
|
||||
{ retries, retryIntervalMs, shouldRetryOnError, shouldRetryOnResponse }: RetryOptions
|
||||
): Promise<Response> {
|
||||
let attemptCount = -1;
|
||||
for (;;) {
|
||||
try {
|
||||
attemptCount += 1;
|
||||
const resp = await fn(attemptCount);
|
||||
if (attemptCount < retries && shouldRetryOnResponse(resp)) {
|
||||
await new Promise(res => setTimeout(res, retryIntervalMs));
|
||||
} else {
|
||||
return resp;
|
||||
}
|
||||
} catch (err: any) {
|
||||
if (attemptCount === retries || !shouldRetryOnError(err)) {
|
||||
throw err;
|
||||
}
|
||||
await new Promise(res => setTimeout(res, retryIntervalMs));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
import { BuildPhase, Generic } from '@expo/eas-build-job';
|
||||
import { Result, asyncResult } from '@expo/results';
|
||||
import { BuildStepGlobalContext, BuildWorkflow, StepsConfigParser, errors } from '@expo/steps';
|
||||
import fs from 'fs/promises';
|
||||
|
||||
import { prepareProjectSourcesAsync } from './common/projectSources';
|
||||
import { BuildContext } from './context';
|
||||
import { CustomBuildContext } from './customBuildContext';
|
||||
import { getEasFunctionGroups } from './steps/easFunctionGroups';
|
||||
import { getEasFunctions } from './steps/easFunctions';
|
||||
import { uploadJobOutputsToWwwAsync } from './utils/outputs';
|
||||
import { retryAsync } from './utils/retry';
|
||||
import { uploadStepMetricsToWwwAsync } from './utils/stepMetrics';
|
||||
|
||||
export async function runGenericJobAsync(
|
||||
ctx: BuildContext<Generic.Job>,
|
||||
{ expoApiV2BaseUrl }: { expoApiV2BaseUrl: string }
|
||||
): Promise<{ runResult: Result<void>; buildWorkflow: BuildWorkflow }> {
|
||||
const customBuildCtx = new CustomBuildContext(ctx);
|
||||
|
||||
await ctx.runBuildPhase(BuildPhase.PREPARE_PROJECT, async () => {
|
||||
await retryAsync(
|
||||
async () => {
|
||||
await fs.rm(customBuildCtx.projectSourceDirectory, { recursive: true, force: true });
|
||||
await fs.mkdir(customBuildCtx.projectSourceDirectory, { recursive: true });
|
||||
|
||||
await prepareProjectSourcesAsync(ctx, customBuildCtx.projectSourceDirectory);
|
||||
},
|
||||
{
|
||||
retryOptions: {
|
||||
retries: 3,
|
||||
retryIntervalMs: 1_000,
|
||||
},
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
const globalContext = new BuildStepGlobalContext(customBuildCtx, false);
|
||||
|
||||
const parser = new StepsConfigParser(globalContext, {
|
||||
externalFunctions: getEasFunctions(customBuildCtx),
|
||||
externalFunctionGroups: getEasFunctionGroups(customBuildCtx),
|
||||
steps: ctx.job.steps,
|
||||
});
|
||||
|
||||
const workflow = await ctx.runBuildPhase(BuildPhase.PARSE_CUSTOM_WORKFLOW_CONFIG, async () => {
|
||||
try {
|
||||
return await parser.parseAsync();
|
||||
} catch (parseError: any) {
|
||||
ctx.logger.error('Failed to parse the job definition file.');
|
||||
if (parseError instanceof errors.BuildWorkflowError) {
|
||||
for (const err of parseError.errors) {
|
||||
ctx.logger.error({ err });
|
||||
}
|
||||
}
|
||||
throw parseError;
|
||||
}
|
||||
});
|
||||
|
||||
const runResult = await asyncResult(workflow.executeAsync());
|
||||
|
||||
await ctx.runBuildPhase(BuildPhase.COMPLETE_JOB, async () => {
|
||||
await uploadJobOutputsToWwwAsync(globalContext, {
|
||||
logger: ctx.logger,
|
||||
expoApiV2BaseUrl,
|
||||
});
|
||||
|
||||
const workflowJobId = globalContext.env.__WORKFLOW_JOB_ID;
|
||||
const robotAccessToken = ctx.job.secrets?.robotAccessToken;
|
||||
|
||||
if (workflowJobId && robotAccessToken) {
|
||||
await uploadStepMetricsToWwwAsync({
|
||||
workflowJobId,
|
||||
robotAccessToken,
|
||||
expoApiV2BaseUrl,
|
||||
stepMetrics: globalContext.stepMetrics,
|
||||
logger: ctx.logger,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return { runResult, buildWorkflow: workflow };
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import * as Builders from './builders';
|
||||
import GCSLoggerStream from './gcs/LoggerStream';
|
||||
import GCS from './gcs/client';
|
||||
|
||||
export { Builders, GCS, GCSLoggerStream };
|
||||
|
||||
export {
|
||||
ArtifactToUpload,
|
||||
Artifacts,
|
||||
BuildContext,
|
||||
BuildContextOptions,
|
||||
CacheManager,
|
||||
LogBuffer,
|
||||
SkipNativeBuildError,
|
||||
} from './context';
|
||||
|
||||
export { PackageManager } from './utils/packageManager';
|
||||
|
||||
export { findAndUploadXcodeBuildLogsAsync } from './ios/xcodeBuildLogs';
|
||||
|
||||
export { Hook, runHookIfPresent } from './utils/hooks';
|
||||
|
||||
export * from './generic';
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,88 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`fastfile createFastfileForResigningBuild should create Fastfile with all variables substituted correctly 1`] = `
|
||||
"lane :do_resign do
|
||||
resign(
|
||||
ipa: "/builds/MyApp.ipa",
|
||||
signing_identity: "iPhone Distribution: Example Inc (ABC123)",
|
||||
provisioning_profile: {
|
||||
"com.example.app" => "/path/to/profiles/main.mobileprovision",
|
||||
"com.example.app.widget" => "/path/to/profiles/widget.mobileprovision",
|
||||
},
|
||||
keychain_path: "/Users/expo/Library/Keychains/login.keychain"
|
||||
)
|
||||
end
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`fastfile createFastfileForResigningBuild should handle empty provisioning profiles 1`] = `
|
||||
"lane :do_resign do
|
||||
resign(
|
||||
ipa: "/tmp/app.ipa",
|
||||
signing_identity: "iPhone Distribution",
|
||||
provisioning_profile: {
|
||||
},
|
||||
keychain_path: "/tmp/keychain"
|
||||
)
|
||||
end
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`fastfile createFastfileForResigningBuild should handle multiple provisioning profiles correctly 1`] = `
|
||||
"lane :do_resign do
|
||||
resign(
|
||||
ipa: "/tmp/app.ipa",
|
||||
signing_identity: "iPhone Distribution: Company Name",
|
||||
provisioning_profile: {
|
||||
"com.example.app" => "/profiles/main.mobileprovision",
|
||||
"com.example.app.widget" => "/profiles/widget.mobileprovision",
|
||||
"com.example.app.extension" => "/profiles/extension.mobileprovision",
|
||||
"com.example.app.intents" => "/profiles/intents.mobileprovision",
|
||||
},
|
||||
keychain_path: "/tmp/keychain"
|
||||
)
|
||||
end
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`fastfile createFastfileForResigningBuild should handle paths with special characters and spaces 1`] = `
|
||||
"lane :do_resign do
|
||||
resign(
|
||||
ipa: "/builds/My App (Release).ipa",
|
||||
signing_identity: "iPhone Distribution: Example Inc (ABC123XYZ)",
|
||||
provisioning_profile: {
|
||||
"com.example.app" => "/path/with spaces/profile (1).mobileprovision",
|
||||
},
|
||||
keychain_path: "/Users/expo/Library/Keychains/login keychain.keychain"
|
||||
)
|
||||
end
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`fastfile createFastfileForResigningBuild should handle single provisioning profile 1`] = `
|
||||
"lane :do_resign do
|
||||
resign(
|
||||
ipa: "/tmp/app.ipa",
|
||||
signing_identity: "iPhone Distribution",
|
||||
provisioning_profile: {
|
||||
"com.example.app" => "/tmp/profile.mobileprovision",
|
||||
},
|
||||
keychain_path: "/tmp/keychain"
|
||||
)
|
||||
end
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`fastfile createFastfileForResigningBuild should produce valid Ruby syntax 1`] = `
|
||||
"lane :do_resign do
|
||||
resign(
|
||||
ipa: "/tmp/app.ipa",
|
||||
signing_identity: "iPhone Distribution",
|
||||
provisioning_profile: {
|
||||
"com.example.app" => "/path/to/profile.mobileprovision",
|
||||
},
|
||||
keychain_path: "/tmp/keychain"
|
||||
)
|
||||
end
|
||||
"
|
||||
`;
|
||||
@@ -0,0 +1,157 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`gymfile createGymfileForArchiveBuild should create Gymfile with all variables substituted correctly 1`] = `
|
||||
"suppress_xcode_output(true)
|
||||
clean(true)
|
||||
|
||||
scheme("MyApp")
|
||||
|
||||
configuration("Release")
|
||||
|
||||
|
||||
export_options({
|
||||
method: "app-store",
|
||||
provisioningProfiles: {
|
||||
"com.example.app" => "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
|
||||
"com.example.app.widget" => "ffffffff-0000-1111-2222-333333333333",
|
||||
}
|
||||
})
|
||||
|
||||
export_xcargs "OTHER_CODE_SIGN_FLAGS=\\"--keychain /Users/expo/Library/Keychains/login.keychain\\""
|
||||
|
||||
disable_xcpretty(true)
|
||||
buildlog_path("/tmp/logs")
|
||||
|
||||
output_directory("/tmp/output")
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`gymfile createGymfileForArchiveBuild should create Gymfile without build configuration when not provided 1`] = `
|
||||
"suppress_xcode_output(true)
|
||||
clean(false)
|
||||
|
||||
scheme("TestScheme")
|
||||
|
||||
|
||||
export_options({
|
||||
method: "ad-hoc",
|
||||
provisioningProfiles: {
|
||||
"com.example.app" => "test-uuid",
|
||||
}
|
||||
})
|
||||
|
||||
export_xcargs "OTHER_CODE_SIGN_FLAGS=\\"--keychain /tmp/keychain\\""
|
||||
|
||||
disable_xcpretty(true)
|
||||
buildlog_path("/tmp/logs")
|
||||
|
||||
output_directory("/tmp/output")
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`gymfile createGymfileForArchiveBuild should handle multiple provisioning profiles correctly 1`] = `
|
||||
"suppress_xcode_output(true)
|
||||
clean(true)
|
||||
|
||||
scheme("MyApp")
|
||||
|
||||
|
||||
export_options({
|
||||
method: "enterprise",
|
||||
provisioningProfiles: {
|
||||
"com.example.app" => "main-app-uuid",
|
||||
"com.example.app.widget" => "widget-uuid",
|
||||
"com.example.app.extension" => "extension-uuid",
|
||||
}
|
||||
})
|
||||
|
||||
export_xcargs "OTHER_CODE_SIGN_FLAGS=\\"--keychain /tmp/keychain\\""
|
||||
|
||||
disable_xcpretty(true)
|
||||
buildlog_path("/tmp/logs")
|
||||
|
||||
output_directory("/tmp/output")
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`gymfile createGymfileForArchiveBuild should include iCloud container environment when provided in entitlements 1`] = `
|
||||
"suppress_xcode_output(true)
|
||||
clean(true)
|
||||
|
||||
scheme("MyApp")
|
||||
|
||||
configuration("Release")
|
||||
|
||||
|
||||
export_options({
|
||||
method: "app-store",
|
||||
provisioningProfiles: {
|
||||
"com.example.app" => "test-uuid",
|
||||
},
|
||||
iCloudContainerEnvironment: "Production"
|
||||
|
||||
})
|
||||
|
||||
export_xcargs "OTHER_CODE_SIGN_FLAGS=\\"--keychain /tmp/keychain\\""
|
||||
|
||||
disable_xcpretty(true)
|
||||
buildlog_path("/tmp/logs")
|
||||
|
||||
output_directory("/tmp/output")
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`gymfile createGymfileForSimulatorBuild should create Gymfile with all simulator variables substituted correctly 1`] = `
|
||||
"suppress_xcode_output(true)
|
||||
clean(true)
|
||||
|
||||
scheme("MyApp")
|
||||
|
||||
configuration("Debug")
|
||||
|
||||
|
||||
derived_data_path("/tmp/derived-data")
|
||||
skip_package_ipa(true)
|
||||
skip_archive(true)
|
||||
destination("generic/platform=iOS Simulator")
|
||||
|
||||
disable_xcpretty(true)
|
||||
buildlog_path("/tmp/logs")
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`gymfile createGymfileForSimulatorBuild should create Gymfile without configuration when not provided 1`] = `
|
||||
"suppress_xcode_output(true)
|
||||
clean(false)
|
||||
|
||||
scheme("TestApp")
|
||||
|
||||
|
||||
derived_data_path("/tmp/derived")
|
||||
skip_package_ipa(true)
|
||||
skip_archive(true)
|
||||
destination("platform=iOS Simulator,name=iPhone 15")
|
||||
|
||||
disable_xcpretty(true)
|
||||
buildlog_path("/tmp/logs")
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`gymfile createGymfileForSimulatorBuild should handle tvOS simulator destination 1`] = `
|
||||
"suppress_xcode_output(true)
|
||||
clean(true)
|
||||
|
||||
scheme("MyTVApp")
|
||||
|
||||
configuration("Debug")
|
||||
|
||||
|
||||
derived_data_path("/tmp/derived")
|
||||
skip_package_ipa(true)
|
||||
skip_archive(true)
|
||||
destination("generic/platform=tvOS Simulator")
|
||||
|
||||
disable_xcpretty(true)
|
||||
buildlog_path("/tmp/logs")
|
||||
"
|
||||
`;
|
||||
@@ -0,0 +1,241 @@
|
||||
import { Ios } from '@expo/eas-build-job';
|
||||
import { vol } from 'memfs';
|
||||
import path from 'path';
|
||||
|
||||
import { configureXcodeProject } from '../configure';
|
||||
import ProvisioningProfile, { DistributionType } from '../credentials/provisioningProfile';
|
||||
|
||||
jest.mock('fs');
|
||||
const originalFs = jest.requireActual('fs');
|
||||
|
||||
afterEach(() => {
|
||||
vol.reset();
|
||||
});
|
||||
|
||||
describe(configureXcodeProject, () => {
|
||||
it('configures credentials for a simple project', async () => {
|
||||
vol.fromJSON(
|
||||
{
|
||||
'ios/testapp.xcodeproj/project.pbxproj': originalFs.readFileSync(
|
||||
path.join(__dirname, 'fixtures/simple-project.pbxproj'),
|
||||
'utf-8'
|
||||
),
|
||||
'ios/testapp/AppDelegate.m': 'placeholder',
|
||||
},
|
||||
'/app'
|
||||
);
|
||||
const options = {
|
||||
credentials: {
|
||||
keychainPath: 'fake/path',
|
||||
targetProvisioningProfiles: {
|
||||
testapp: {
|
||||
path: 'fake/path.mobileprovision',
|
||||
target: 'testapp',
|
||||
bundleIdentifier: 'abc',
|
||||
teamId: 'ABCDEFGH',
|
||||
uuid: 'abc',
|
||||
name: 'profile name',
|
||||
developerCertificate: Buffer.from('test'),
|
||||
certificateCommonName: 'Abc 123',
|
||||
distributionType: DistributionType.APP_STORE,
|
||||
},
|
||||
},
|
||||
distributionType: DistributionType.APP_STORE,
|
||||
teamId: 'ABCDEFGH',
|
||||
applicationTargetProvisioningProfile: {} as ProvisioningProfile<Ios.Job>,
|
||||
},
|
||||
buildConfiguration: 'Release',
|
||||
};
|
||||
const ctx = {
|
||||
getReactNativeProjectDirectory: () => '/app',
|
||||
logger: { info: jest.fn() },
|
||||
job: {},
|
||||
};
|
||||
await configureXcodeProject(ctx as any, options);
|
||||
expect(
|
||||
vol.readFileSync('/app/ios/testapp.xcodeproj/project.pbxproj', 'utf-8')
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
it('configures credentials for multi target project', async () => {
|
||||
vol.fromJSON(
|
||||
{
|
||||
'ios/testapp.xcodeproj/project.pbxproj': originalFs.readFileSync(
|
||||
path.join(__dirname, 'fixtures/multitarget-project.pbxproj'),
|
||||
'utf-8'
|
||||
),
|
||||
'ios/testapp/AppDelegate.m': 'placeholder',
|
||||
},
|
||||
'/app'
|
||||
);
|
||||
const options = {
|
||||
credentials: {
|
||||
keychainPath: 'fake/path',
|
||||
targetProvisioningProfiles: {
|
||||
shareextension: {
|
||||
path: 'fake/path1.mobileprovision',
|
||||
target: 'shareextension',
|
||||
bundleIdentifier: 'abc.extension',
|
||||
teamId: 'ABCDEFGH',
|
||||
uuid: 'abc',
|
||||
name: 'extension profile',
|
||||
developerCertificate: Buffer.from('test'),
|
||||
certificateCommonName: 'Abc 123',
|
||||
distributionType: DistributionType.APP_STORE,
|
||||
},
|
||||
multitarget: {
|
||||
path: 'fake/path2.mobileprovision',
|
||||
target: 'multitarget',
|
||||
bundleIdentifier: 'abc',
|
||||
teamId: 'ABCDEFGH',
|
||||
uuid: 'abc',
|
||||
name: 'multitarget profile',
|
||||
developerCertificate: Buffer.from('test'),
|
||||
certificateCommonName: 'Abc 123',
|
||||
distributionType: DistributionType.APP_STORE,
|
||||
},
|
||||
},
|
||||
distributionType: DistributionType.APP_STORE,
|
||||
teamId: 'ABCDEFGH',
|
||||
applicationTargetProvisioningProfile: {} as ProvisioningProfile<Ios.Job>,
|
||||
},
|
||||
buildConfiguration: 'Release',
|
||||
};
|
||||
const ctx = {
|
||||
getReactNativeProjectDirectory: () => '/app',
|
||||
logger: { info: jest.fn() },
|
||||
job: {},
|
||||
};
|
||||
await configureXcodeProject(ctx as any, options);
|
||||
expect(
|
||||
vol.readFileSync('/app/ios/testapp.xcodeproj/project.pbxproj', 'utf-8')
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
it('configures credentials and versions for a simple project', async () => {
|
||||
vol.fromJSON(
|
||||
{
|
||||
'ios/testapp/Info.plist': originalFs.readFileSync(
|
||||
path.join(__dirname, 'fixtures/Info.plist'),
|
||||
'utf-8'
|
||||
),
|
||||
'ios/testapp.xcodeproj/project.pbxproj': originalFs.readFileSync(
|
||||
path.join(__dirname, 'fixtures/simple-project.pbxproj'),
|
||||
'utf-8'
|
||||
),
|
||||
'ios/testapp/AppDelegate.m': 'placeholder',
|
||||
},
|
||||
'/app'
|
||||
);
|
||||
const options = {
|
||||
credentials: {
|
||||
keychainPath: 'fake/path',
|
||||
targetProvisioningProfiles: {
|
||||
testapp: {
|
||||
path: 'fake/path.mobileprovision',
|
||||
target: 'testapp',
|
||||
bundleIdentifier: 'abc',
|
||||
teamId: 'ABCDEFGH',
|
||||
uuid: 'abc',
|
||||
name: 'profile name',
|
||||
developerCertificate: Buffer.from('test'),
|
||||
certificateCommonName: 'Abc 123',
|
||||
distributionType: DistributionType.APP_STORE,
|
||||
},
|
||||
},
|
||||
distributionType: DistributionType.APP_STORE,
|
||||
teamId: 'ABCDEFGH',
|
||||
applicationTargetProvisioningProfile: {} as ProvisioningProfile<Ios.Job>,
|
||||
},
|
||||
buildConfiguration: 'Release',
|
||||
};
|
||||
const ctx = {
|
||||
getReactNativeProjectDirectory: () => '/app',
|
||||
logger: { info: jest.fn() },
|
||||
job: {
|
||||
version: {
|
||||
appVersion: '1.2.3',
|
||||
buildNumber: '1.2.4',
|
||||
},
|
||||
},
|
||||
};
|
||||
await configureXcodeProject(ctx as any, options);
|
||||
expect(
|
||||
vol.readFileSync('/app/ios/testapp.xcodeproj/project.pbxproj', 'utf-8')
|
||||
).toMatchSnapshot();
|
||||
expect(vol.readFileSync('/app/ios/testapp/Info.plist', 'utf-8')).toMatchSnapshot(
|
||||
'Info.plist application target'
|
||||
);
|
||||
});
|
||||
it('configures credentials and versions for multi target project', async () => {
|
||||
vol.fromJSON(
|
||||
{
|
||||
'ios/multitarget/Info.plist': originalFs.readFileSync(
|
||||
path.join(__dirname, 'fixtures/Info.plist'),
|
||||
'utf-8'
|
||||
),
|
||||
'ios/shareextension/Info.plist': originalFs.readFileSync(
|
||||
path.join(__dirname, 'fixtures/Info.plist'),
|
||||
'utf-8'
|
||||
),
|
||||
'ios/testapp.xcodeproj/project.pbxproj': originalFs.readFileSync(
|
||||
path.join(__dirname, 'fixtures/multitarget-project.pbxproj'),
|
||||
'utf-8'
|
||||
),
|
||||
'ios/testapp/AppDelegate.m': 'placeholder',
|
||||
},
|
||||
'/app'
|
||||
);
|
||||
const options = {
|
||||
credentials: {
|
||||
keychainPath: 'fake/path',
|
||||
targetProvisioningProfiles: {
|
||||
shareextension: {
|
||||
path: 'fake/path1.mobileprovision',
|
||||
target: 'shareextension',
|
||||
bundleIdentifier: 'abc.extension',
|
||||
teamId: 'ABCDEFGH',
|
||||
uuid: 'abc',
|
||||
name: 'extension profile',
|
||||
developerCertificate: Buffer.from('test'),
|
||||
certificateCommonName: 'Abc 123',
|
||||
distributionType: DistributionType.APP_STORE,
|
||||
},
|
||||
multitarget: {
|
||||
path: 'fake/path2.mobileprovision',
|
||||
target: 'multitarget',
|
||||
bundleIdentifier: 'abc',
|
||||
teamId: 'ABCDEFGH',
|
||||
uuid: 'abc',
|
||||
name: 'multitarget profile',
|
||||
developerCertificate: Buffer.from('test'),
|
||||
certificateCommonName: 'Abc 123',
|
||||
distributionType: DistributionType.APP_STORE,
|
||||
},
|
||||
},
|
||||
distributionType: DistributionType.APP_STORE,
|
||||
teamId: 'ABCDEFGH',
|
||||
applicationTargetProvisioningProfile: {} as ProvisioningProfile<Ios.Job>,
|
||||
},
|
||||
buildConfiguration: 'Release',
|
||||
};
|
||||
const ctx = {
|
||||
getReactNativeProjectDirectory: () => '/app',
|
||||
logger: { info: jest.fn() },
|
||||
job: {
|
||||
version: {
|
||||
appVersion: '1.2.3',
|
||||
buildNumber: '1.2.4',
|
||||
},
|
||||
},
|
||||
};
|
||||
await configureXcodeProject(ctx as any, options);
|
||||
expect(
|
||||
vol.readFileSync('/app/ios/testapp.xcodeproj/project.pbxproj', 'utf-8')
|
||||
).toMatchSnapshot();
|
||||
expect(vol.readFileSync('/app/ios/shareextension/Info.plist', 'utf-8')).toMatchSnapshot(
|
||||
'Info.plist application target'
|
||||
);
|
||||
expect(vol.readFileSync('/app/ios/multitarget/Info.plist', 'utf-8')).toMatchSnapshot(
|
||||
'Info.plist extension'
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,169 @@
|
||||
import plist from '@expo/plist';
|
||||
import fs from 'fs-extra';
|
||||
import { vol } from 'memfs';
|
||||
|
||||
import {
|
||||
IosMetadataName,
|
||||
iosGetNativelyDefinedChannelAsync,
|
||||
iosGetNativelyDefinedRuntimeVersionAsync,
|
||||
iosSetChannelNativelyAsync,
|
||||
iosSetRuntimeVersionNativelyAsync,
|
||||
} from '../../ios/expoUpdates';
|
||||
|
||||
jest.mock('fs');
|
||||
|
||||
const expoPlistPath = '/app/ios/testapp/Supporting/Expo.plist';
|
||||
const noItemsExpoPlist = `
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
</dict>
|
||||
</plist>`;
|
||||
const channel = 'easupdatechannel';
|
||||
|
||||
afterEach(() => {
|
||||
vol.reset();
|
||||
});
|
||||
|
||||
describe(iosSetChannelNativelyAsync, () => {
|
||||
it('sets the channel', async () => {
|
||||
vol.fromJSON(
|
||||
{
|
||||
'ios/testapp/Supporting/Expo.plist': noItemsExpoPlist,
|
||||
'ios/testapp.xcodeproj/project.pbxproj': 'placeholder',
|
||||
'ios/testapp/AppDelegate.m': 'placeholder',
|
||||
},
|
||||
'/app'
|
||||
);
|
||||
|
||||
const ctx = {
|
||||
getReactNativeProjectDirectory: () => '/app',
|
||||
job: { updates: { channel } },
|
||||
logger: { info: () => {} },
|
||||
};
|
||||
await iosSetChannelNativelyAsync(ctx as any);
|
||||
|
||||
const newExpoPlist = await fs.readFile(expoPlistPath, 'utf8');
|
||||
expect(
|
||||
plist.parse(newExpoPlist)[IosMetadataName.UPDATES_CONFIGURATION_REQUEST_HEADERS_KEY]
|
||||
).toEqual({ 'expo-channel-name': channel });
|
||||
});
|
||||
});
|
||||
|
||||
describe(iosGetNativelyDefinedChannelAsync, () => {
|
||||
it('gets the channel', async () => {
|
||||
const expoPlist = `
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>EXUpdatesRequestHeaders</key>
|
||||
<dict>
|
||||
<key>expo-channel-name</key>
|
||||
<string>staging-123</string>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
`;
|
||||
|
||||
vol.fromJSON(
|
||||
{
|
||||
'ios/testapp/Supporting/Expo.plist': expoPlist,
|
||||
'ios/testapp.xcodeproj/project.pbxproj': 'placeholder',
|
||||
'ios/testapp/AppDelegate.m': 'placeholder',
|
||||
},
|
||||
'/app'
|
||||
);
|
||||
|
||||
const ctx = {
|
||||
getReactNativeProjectDirectory: () => '/app',
|
||||
logger: { info: () => {} },
|
||||
};
|
||||
await expect(iosGetNativelyDefinedChannelAsync(ctx as any)).resolves.toBe('staging-123');
|
||||
});
|
||||
});
|
||||
|
||||
describe(iosGetNativelyDefinedRuntimeVersionAsync, () => {
|
||||
it('gets the natively defined runtime version', async () => {
|
||||
const expoPlist = `
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>EXUpdatesRuntimeVersion</key>
|
||||
<string>4.5.6</string>
|
||||
</dict>
|
||||
</plist>`;
|
||||
|
||||
vol.fromJSON(
|
||||
{
|
||||
'ios/testapp/Supporting/Expo.plist': expoPlist,
|
||||
'ios/testapp.xcodeproj/project.pbxproj': 'placeholder',
|
||||
'ios/testapp/AppDelegate.m': 'placeholder',
|
||||
},
|
||||
'/app'
|
||||
);
|
||||
|
||||
const ctx = {
|
||||
getReactNativeProjectDirectory: () => '/app',
|
||||
logger: { info: () => {} },
|
||||
};
|
||||
|
||||
const nativelyDefinedRuntimeVersion = await iosGetNativelyDefinedRuntimeVersionAsync(
|
||||
ctx as any
|
||||
);
|
||||
expect(nativelyDefinedRuntimeVersion).toBe('4.5.6');
|
||||
});
|
||||
});
|
||||
|
||||
describe(iosSetRuntimeVersionNativelyAsync, () => {
|
||||
it("sets runtime version if it's not specified", async () => {
|
||||
vol.fromJSON(
|
||||
{
|
||||
'ios/testapp/Supporting/Expo.plist': noItemsExpoPlist,
|
||||
'ios/testapp.xcodeproj/project.pbxproj': 'placeholder',
|
||||
'ios/testapp/AppDelegate.m': 'placeholder',
|
||||
},
|
||||
'/app'
|
||||
);
|
||||
const ctx = {
|
||||
getReactNativeProjectDirectory: () => '/app',
|
||||
logger: { info: () => {} },
|
||||
};
|
||||
|
||||
await iosSetRuntimeVersionNativelyAsync(ctx as any, '1.2.3');
|
||||
|
||||
const newExpoPlist = await fs.readFile(expoPlistPath, 'utf8');
|
||||
expect(plist.parse(newExpoPlist)[IosMetadataName.RUNTIME_VERSION]).toEqual('1.2.3');
|
||||
});
|
||||
it("updates runtime version if it's already defined", async () => {
|
||||
const expoPlist = `
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>RELEASE_CHANNEL</key>
|
||||
<string>examplereleasechannel</string>
|
||||
</dict>
|
||||
</plist>`;
|
||||
|
||||
vol.fromJSON(
|
||||
{
|
||||
'ios/testapp/Supporting/Expo.plist': expoPlist,
|
||||
'ios/testapp.xcodeproj/project.pbxproj': 'placeholder',
|
||||
'ios/testapp/AppDelegate.m': 'placeholder',
|
||||
},
|
||||
'/app'
|
||||
);
|
||||
const ctx = {
|
||||
getReactNativeProjectDirectory: () => '/app',
|
||||
logger: { info: () => {} },
|
||||
};
|
||||
|
||||
await iosSetRuntimeVersionNativelyAsync(ctx as any, '1.2.3');
|
||||
|
||||
const newExpoPlist = await fs.readFile(expoPlistPath, 'utf8');
|
||||
expect(plist.parse(newExpoPlist)[IosMetadataName.RUNTIME_VERSION]).toEqual('1.2.3');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,172 @@
|
||||
import { vol } from 'memfs';
|
||||
|
||||
import { TargetProvisioningProfiles } from '../credentials/manager';
|
||||
import { createFastfileForResigningBuild } from '../fastfile';
|
||||
|
||||
describe('fastfile', () => {
|
||||
beforeEach(() => {
|
||||
vol.reset();
|
||||
// Set up /tmp directory in the mock filesystem
|
||||
vol.fromJSON({
|
||||
'/tmp/.keep': '', // Create /tmp directory
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vol.reset();
|
||||
});
|
||||
|
||||
describe('createFastfileForResigningBuild', () => {
|
||||
it('should create Fastfile with all variables substituted correctly', async () => {
|
||||
const mockTargetProvisioningProfiles: TargetProvisioningProfiles = {
|
||||
'com.example.app': {
|
||||
bundleIdentifier: 'com.example.app',
|
||||
uuid: 'test-uuid-1',
|
||||
path: '/path/to/profiles/main.mobileprovision',
|
||||
} as any,
|
||||
'com.example.app.widget': {
|
||||
bundleIdentifier: 'com.example.app.widget',
|
||||
uuid: 'test-uuid-2',
|
||||
path: '/path/to/profiles/widget.mobileprovision',
|
||||
} as any,
|
||||
};
|
||||
|
||||
const outputFile = '/tmp/Fastfile';
|
||||
|
||||
await createFastfileForResigningBuild({
|
||||
outputFile,
|
||||
ipaPath: '/builds/MyApp.ipa',
|
||||
signingIdentity: 'iPhone Distribution: Example Inc (ABC123)',
|
||||
keychainPath: '/Users/expo/Library/Keychains/login.keychain',
|
||||
targetProvisioningProfiles: mockTargetProvisioningProfiles,
|
||||
});
|
||||
|
||||
const generatedContent = vol.readFileSync(outputFile, 'utf-8') as string;
|
||||
expect(generatedContent).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should handle single provisioning profile', async () => {
|
||||
const mockTargetProvisioningProfiles: TargetProvisioningProfiles = {
|
||||
'com.example.app': {
|
||||
bundleIdentifier: 'com.example.app',
|
||||
uuid: 'single-uuid',
|
||||
path: '/tmp/profile.mobileprovision',
|
||||
} as any,
|
||||
};
|
||||
|
||||
const outputFile = '/tmp/Fastfile';
|
||||
|
||||
await createFastfileForResigningBuild({
|
||||
outputFile,
|
||||
ipaPath: '/tmp/app.ipa',
|
||||
signingIdentity: 'iPhone Distribution',
|
||||
keychainPath: '/tmp/keychain',
|
||||
targetProvisioningProfiles: mockTargetProvisioningProfiles,
|
||||
});
|
||||
|
||||
const generatedContent = vol.readFileSync(outputFile, 'utf-8') as string;
|
||||
expect(generatedContent).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should handle multiple provisioning profiles correctly', async () => {
|
||||
const mockTargetProvisioningProfiles: TargetProvisioningProfiles = {
|
||||
'com.example.app': {
|
||||
bundleIdentifier: 'com.example.app',
|
||||
uuid: 'uuid-main',
|
||||
path: '/profiles/main.mobileprovision',
|
||||
} as any,
|
||||
'com.example.app.widget': {
|
||||
bundleIdentifier: 'com.example.app.widget',
|
||||
uuid: 'uuid-widget',
|
||||
path: '/profiles/widget.mobileprovision',
|
||||
} as any,
|
||||
'com.example.app.extension': {
|
||||
bundleIdentifier: 'com.example.app.extension',
|
||||
uuid: 'uuid-extension',
|
||||
path: '/profiles/extension.mobileprovision',
|
||||
} as any,
|
||||
'com.example.app.intents': {
|
||||
bundleIdentifier: 'com.example.app.intents',
|
||||
uuid: 'uuid-intents',
|
||||
path: '/profiles/intents.mobileprovision',
|
||||
} as any,
|
||||
};
|
||||
|
||||
const outputFile = '/tmp/Fastfile';
|
||||
|
||||
await createFastfileForResigningBuild({
|
||||
outputFile,
|
||||
ipaPath: '/tmp/app.ipa',
|
||||
signingIdentity: 'iPhone Distribution: Company Name',
|
||||
keychainPath: '/tmp/keychain',
|
||||
targetProvisioningProfiles: mockTargetProvisioningProfiles,
|
||||
});
|
||||
|
||||
const generatedContent = vol.readFileSync(outputFile, 'utf-8') as string;
|
||||
expect(generatedContent).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should handle empty provisioning profiles', async () => {
|
||||
const mockTargetProvisioningProfiles: TargetProvisioningProfiles = {};
|
||||
|
||||
const outputFile = '/tmp/Fastfile';
|
||||
|
||||
await createFastfileForResigningBuild({
|
||||
outputFile,
|
||||
ipaPath: '/tmp/app.ipa',
|
||||
signingIdentity: 'iPhone Distribution',
|
||||
keychainPath: '/tmp/keychain',
|
||||
targetProvisioningProfiles: mockTargetProvisioningProfiles,
|
||||
});
|
||||
|
||||
const generatedContent = vol.readFileSync(outputFile, 'utf-8') as string;
|
||||
expect(generatedContent).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should handle paths with special characters and spaces', async () => {
|
||||
const mockTargetProvisioningProfiles: TargetProvisioningProfiles = {
|
||||
'com.example.app': {
|
||||
bundleIdentifier: 'com.example.app',
|
||||
uuid: 'test-uuid',
|
||||
path: '/path/with spaces/profile (1).mobileprovision',
|
||||
} as any,
|
||||
};
|
||||
|
||||
const outputFile = '/tmp/Fastfile';
|
||||
|
||||
await createFastfileForResigningBuild({
|
||||
outputFile,
|
||||
ipaPath: '/builds/My App (Release).ipa',
|
||||
signingIdentity: 'iPhone Distribution: Example Inc (ABC123XYZ)',
|
||||
keychainPath: '/Users/expo/Library/Keychains/login keychain.keychain',
|
||||
targetProvisioningProfiles: mockTargetProvisioningProfiles,
|
||||
});
|
||||
|
||||
const generatedContent = vol.readFileSync(outputFile, 'utf-8') as string;
|
||||
expect(generatedContent).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should produce valid Ruby syntax', async () => {
|
||||
const mockTargetProvisioningProfiles: TargetProvisioningProfiles = {
|
||||
'com.example.app': {
|
||||
bundleIdentifier: 'com.example.app',
|
||||
uuid: 'test-uuid',
|
||||
path: '/path/to/profile.mobileprovision',
|
||||
} as any,
|
||||
};
|
||||
|
||||
const outputFile = '/tmp/Fastfile';
|
||||
|
||||
await createFastfileForResigningBuild({
|
||||
outputFile,
|
||||
ipaPath: '/tmp/app.ipa',
|
||||
signingIdentity: 'iPhone Distribution',
|
||||
keychainPath: '/tmp/keychain',
|
||||
targetProvisioningProfiles: mockTargetProvisioningProfiles,
|
||||
});
|
||||
|
||||
const generatedContent = vol.readFileSync(outputFile, 'utf-8') as string;
|
||||
expect(generatedContent).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,76 @@
|
||||
<?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>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>testapp</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0.0</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>org.reactjs.native.example.testapp.turtlev2</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>NSAppTransportSecurity</key>
|
||||
<dict>
|
||||
<key>NSAllowsArbitraryLoads</key>
|
||||
<true/>
|
||||
<key>NSExceptionDomains</key>
|
||||
<dict>
|
||||
<key>localhost</key>
|
||||
<dict>
|
||||
<key>NSExceptionAllowsInsecureHTTPLoads</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>SplashScreen</string>
|
||||
<key>UIRequiredDeviceCapabilities</key>
|
||||
<array>
|
||||
<string>armv7</string>
|
||||
</array>
|
||||
<key>UIRequiresFullScreen</key>
|
||||
<false/>
|
||||
<key>UIStatusBarStyle</key>
|
||||
<string>UIStatusBarStyleDefault</string>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UIUserInterfaceStyle</key>
|
||||
<string>Light</string>
|
||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||
<false/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,655 @@
|
||||
// !$*UTF8*$!
|
||||
{
|
||||
archiveVersion = 1;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 46;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
13B07FBC1A68108700A75B9A /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB01A68108700A75B9A /* AppDelegate.m */; };
|
||||
13B07FBD1A68108700A75B9A /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB11A68108700A75B9A /* LaunchScreen.xib */; };
|
||||
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; };
|
||||
13B07FC11A68108700A75B9A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; };
|
||||
3E461D99554A48A4959DE609 /* SplashScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */; };
|
||||
6ADAD6E626493A420001F56E /* ShareViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 6ADAD6E526493A420001F56E /* ShareViewController.m */; };
|
||||
6ADAD6E926493A420001F56E /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 6ADAD6E726493A420001F56E /* MainInterface.storyboard */; };
|
||||
6ADAD6ED26493A420001F56E /* shareextension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 6ADAD6E226493A420001F56E /* shareextension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||
96905EF65AED1B983A6B3ABC /* libPods-multitarget.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 58EEBF8E8E6FB1BC6CAF49B5 /* libPods-multitarget.a */; };
|
||||
BB2F792D24A3F905000567C9 /* Expo.plist in Resources */ = {isa = PBXBuildFile; fileRef = BB2F792C24A3F905000567C9 /* Expo.plist */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
6ADAD6EB26493A420001F56E /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = 83CBB9F71A601CBA00E9B192 /* Project object */;
|
||||
proxyType = 1;
|
||||
remoteGlobalIDString = 6ADAD6E126493A420001F56E;
|
||||
remoteInfo = shareextension;
|
||||
};
|
||||
/* End PBXContainerItemProxy section */
|
||||
|
||||
/* Begin PBXCopyFilesBuildPhase section */
|
||||
6ADAD6F126493A420001F56E /* Embed App Extensions */ = {
|
||||
isa = PBXCopyFilesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
dstPath = "";
|
||||
dstSubfolderSpec = 13;
|
||||
files = (
|
||||
6ADAD6ED26493A420001F56E /* shareextension.appex in Embed App Extensions */,
|
||||
);
|
||||
name = "Embed App Extensions";
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXCopyFilesBuildPhase section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
008F07F21AC5B25A0029DE68 /* main.jsbundle */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = main.jsbundle; sourceTree = "<group>"; };
|
||||
13B07F961A680F5B00A75B9A /* multitarget.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = multitarget.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
13B07FAF1A68108700A75B9A /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = AppDelegate.h; path = multitarget/AppDelegate.h; sourceTree = "<group>"; };
|
||||
13B07FB01A68108700A75B9A /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = AppDelegate.m; path = multitarget/AppDelegate.m; sourceTree = "<group>"; };
|
||||
13B07FB21A68108700A75B9A /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/LaunchScreen.xib; sourceTree = "<group>"; };
|
||||
13B07FB51A68108700A75B9A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = multitarget/Images.xcassets; sourceTree = "<group>"; };
|
||||
13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = multitarget/Info.plist; sourceTree = "<group>"; };
|
||||
13B07FB71A68108700A75B9A /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = main.m; path = multitarget/main.m; sourceTree = "<group>"; };
|
||||
58EEBF8E8E6FB1BC6CAF49B5 /* libPods-multitarget.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-multitarget.a"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
6ADAD6E226493A420001F56E /* shareextension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = shareextension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
6ADAD6E426493A420001F56E /* ShareViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ShareViewController.h; sourceTree = "<group>"; };
|
||||
6ADAD6E526493A420001F56E /* ShareViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ShareViewController.m; sourceTree = "<group>"; };
|
||||
6ADAD6E826493A420001F56E /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/MainInterface.storyboard; sourceTree = "<group>"; };
|
||||
6ADAD6EA26493A420001F56E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
6C2E3173556A471DD304B334 /* Pods-multitarget.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-multitarget.debug.xcconfig"; path = "Target Support Files/Pods-multitarget/Pods-multitarget.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
7A4D352CD337FB3A3BF06240 /* Pods-multitarget.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-multitarget.release.xcconfig"; path = "Target Support Files/Pods-multitarget/Pods-multitarget.release.xcconfig"; sourceTree = "<group>"; };
|
||||
AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = SplashScreen.storyboard; path = multitarget/SplashScreen.storyboard; sourceTree = "<group>"; };
|
||||
BB2F792C24A3F905000567C9 /* Expo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Expo.plist; sourceTree = "<group>"; };
|
||||
ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; };
|
||||
ED2971642150620600B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = Platforms/AppleTVOS.platform/Developer/SDKs/AppleTVOS12.0.sdk/System/Library/Frameworks/JavaScriptCore.framework; sourceTree = DEVELOPER_DIR; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
13B07F8C1A680F5B00A75B9A /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
96905EF65AED1B983A6B3ABC /* libPods-multitarget.a in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
6ADAD6DF26493A420001F56E /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
13B07FAE1A68108700A75B9A /* multitarget */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
BB2F792B24A3F905000567C9 /* Supporting */,
|
||||
008F07F21AC5B25A0029DE68 /* main.jsbundle */,
|
||||
13B07FAF1A68108700A75B9A /* AppDelegate.h */,
|
||||
13B07FB01A68108700A75B9A /* AppDelegate.m */,
|
||||
13B07FB51A68108700A75B9A /* Images.xcassets */,
|
||||
13B07FB61A68108700A75B9A /* Info.plist */,
|
||||
13B07FB11A68108700A75B9A /* LaunchScreen.xib */,
|
||||
13B07FB71A68108700A75B9A /* main.m */,
|
||||
AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */,
|
||||
);
|
||||
name = multitarget;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
2D16E6871FA4F8E400B85C8A /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
ED297162215061F000B7C4FE /* JavaScriptCore.framework */,
|
||||
ED2971642150620600B7C4FE /* JavaScriptCore.framework */,
|
||||
58EEBF8E8E6FB1BC6CAF49B5 /* libPods-multitarget.a */,
|
||||
);
|
||||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
6ADAD6E326493A420001F56E /* shareextension */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
6ADAD6E426493A420001F56E /* ShareViewController.h */,
|
||||
6ADAD6E526493A420001F56E /* ShareViewController.m */,
|
||||
6ADAD6E726493A420001F56E /* MainInterface.storyboard */,
|
||||
6ADAD6EA26493A420001F56E /* Info.plist */,
|
||||
);
|
||||
path = shareextension;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
832341AE1AAA6A7D00B99B32 /* Libraries */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
);
|
||||
name = Libraries;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
83CBB9F61A601CBA00E9B192 = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
13B07FAE1A68108700A75B9A /* multitarget */,
|
||||
832341AE1AAA6A7D00B99B32 /* Libraries */,
|
||||
6ADAD6E326493A420001F56E /* shareextension */,
|
||||
83CBBA001A601CBA00E9B192 /* Products */,
|
||||
2D16E6871FA4F8E400B85C8A /* Frameworks */,
|
||||
D65327D7A22EEC0BE12398D9 /* Pods */,
|
||||
);
|
||||
indentWidth = 2;
|
||||
sourceTree = "<group>";
|
||||
tabWidth = 2;
|
||||
usesTabs = 0;
|
||||
};
|
||||
83CBBA001A601CBA00E9B192 /* Products */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
13B07F961A680F5B00A75B9A /* multitarget.app */,
|
||||
6ADAD6E226493A420001F56E /* shareextension.appex */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
BB2F792B24A3F905000567C9 /* Supporting */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
BB2F792C24A3F905000567C9 /* Expo.plist */,
|
||||
);
|
||||
name = Supporting;
|
||||
path = multitarget/Supporting;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D65327D7A22EEC0BE12398D9 /* Pods */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
6C2E3173556A471DD304B334 /* Pods-multitarget.debug.xcconfig */,
|
||||
7A4D352CD337FB3A3BF06240 /* Pods-multitarget.release.xcconfig */,
|
||||
);
|
||||
path = Pods;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
13B07F861A680F5B00A75B9A /* multitarget */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "multitarget" */;
|
||||
buildPhases = (
|
||||
08A4A3CD28434E44B6B9DE2E /* [CP] Check Pods Manifest.lock */,
|
||||
FD10A7F022414F080027D42C /* Start Packager */,
|
||||
13B07F871A680F5B00A75B9A /* Sources */,
|
||||
13B07F8C1A680F5B00A75B9A /* Frameworks */,
|
||||
13B07F8E1A680F5B00A75B9A /* Resources */,
|
||||
00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */,
|
||||
AF7BF8C823CE838DA4182532 /* [CP] Copy Pods Resources */,
|
||||
6ADAD6F126493A420001F56E /* Embed App Extensions */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
6ADAD6EC26493A420001F56E /* PBXTargetDependency */,
|
||||
);
|
||||
name = multitarget;
|
||||
productName = multitarget;
|
||||
productReference = 13B07F961A680F5B00A75B9A /* multitarget.app */;
|
||||
productType = "com.apple.product-type.application";
|
||||
};
|
||||
6ADAD6E126493A420001F56E /* shareextension */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 6ADAD6EE26493A420001F56E /* Build configuration list for PBXNativeTarget "shareextension" */;
|
||||
buildPhases = (
|
||||
6ADAD6DE26493A420001F56E /* Sources */,
|
||||
6ADAD6DF26493A420001F56E /* Frameworks */,
|
||||
6ADAD6E026493A420001F56E /* Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
);
|
||||
name = shareextension;
|
||||
productName = shareextension;
|
||||
productReference = 6ADAD6E226493A420001F56E /* shareextension.appex */;
|
||||
productType = "com.apple.product-type.app-extension";
|
||||
};
|
||||
/* End PBXNativeTarget section */
|
||||
|
||||
/* Begin PBXProject section */
|
||||
83CBB9F71A601CBA00E9B192 /* Project object */ = {
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
LastUpgradeCheck = 1130;
|
||||
TargetAttributes = {
|
||||
13B07F861A680F5B00A75B9A = {
|
||||
DevelopmentTeam = QL76XYH73P;
|
||||
LastSwiftMigration = 1120;
|
||||
};
|
||||
6ADAD6E126493A420001F56E = {
|
||||
CreatedOnToolsVersion = 12.5;
|
||||
DevelopmentTeam = QL76XYH73P;
|
||||
ProvisioningStyle = Automatic;
|
||||
};
|
||||
};
|
||||
};
|
||||
buildConfigurationList = 83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "multitarget" */;
|
||||
compatibilityVersion = "Xcode 3.2";
|
||||
developmentRegion = en;
|
||||
hasScannedForEncodings = 0;
|
||||
knownRegions = (
|
||||
en,
|
||||
Base,
|
||||
);
|
||||
mainGroup = 83CBB9F61A601CBA00E9B192;
|
||||
productRefGroup = 83CBBA001A601CBA00E9B192 /* Products */;
|
||||
projectDirPath = "";
|
||||
projectRoot = "";
|
||||
targets = (
|
||||
13B07F861A680F5B00A75B9A /* multitarget */,
|
||||
6ADAD6E126493A420001F56E /* shareextension */,
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
|
||||
/* Begin PBXResourcesBuildPhase section */
|
||||
13B07F8E1A680F5B00A75B9A /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
BB2F792D24A3F905000567C9 /* Expo.plist in Resources */,
|
||||
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */,
|
||||
13B07FBD1A68108700A75B9A /* LaunchScreen.xib in Resources */,
|
||||
3E461D99554A48A4959DE609 /* SplashScreen.storyboard in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
6ADAD6E026493A420001F56E /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
6ADAD6E926493A420001F56E /* MainInterface.storyboard in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXResourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXShellScriptBuildPhase section */
|
||||
00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputPaths = (
|
||||
);
|
||||
name = "Bundle React Native code and images";
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "if [[ -f /Users/dsokal/.volta/bin/node ]]; then\n export NODE_BINARY=/Users/dsokal/.volta/bin/node\nelse\n export NODE_BINARY=node\nfi\n../node_modules/react-native/scripts/react-native-xcode.sh\n../node_modules/expo-constants/scripts/get-app-config-ios.sh\n../node_modules/expo-updates/scripts/create-manifest-ios.sh\n";
|
||||
};
|
||||
08A4A3CD28434E44B6B9DE2E /* [CP] Check Pods Manifest.lock */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
);
|
||||
inputPaths = (
|
||||
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
|
||||
"${PODS_ROOT}/Manifest.lock",
|
||||
);
|
||||
name = "[CP] Check Pods Manifest.lock";
|
||||
outputFileListPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
"$(DERIVED_FILE_DIR)/Pods-multitarget-checkManifestLockResult.txt",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
AF7BF8C823CE838DA4182532 /* [CP] Copy Pods Resources */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-multitarget/Pods-multitarget-resources.sh",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/React-Core/AccessibilityResources.bundle",
|
||||
);
|
||||
name = "[CP] Copy Pods Resources";
|
||||
outputPaths = (
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/AccessibilityResources.bundle",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-multitarget/Pods-multitarget-resources.sh\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
FD10A7F022414F080027D42C /* Start Packager */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
);
|
||||
inputPaths = (
|
||||
);
|
||||
name = "Start Packager";
|
||||
outputFileListPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "export RCT_METRO_PORT=\"${RCT_METRO_PORT:=8081}\"\necho \"export RCT_METRO_PORT=${RCT_METRO_PORT}\" > \"${SRCROOT}/../node_modules/react-native/scripts/.packager.env\"\nif [ -z \"${RCT_NO_LAUNCH_PACKAGER+xxx}\" ] ; then\n if nc -w 5 -z localhost ${RCT_METRO_PORT} ; then\n if ! curl -s \"http://localhost:${RCT_METRO_PORT}/status\" | grep -q \"packager-status:running\" ; then\n echo \"Port ${RCT_METRO_PORT} already in use, packager is either not running or not running correctly\"\n exit 2\n fi\n else\n open \"$SRCROOT/../node_modules/react-native/scripts/launchPackager.command\" || echo \"Can't start packager automatically\"\n fi\nfi\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
/* End PBXShellScriptBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
13B07F871A680F5B00A75B9A /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
13B07FBC1A68108700A75B9A /* AppDelegate.m in Sources */,
|
||||
13B07FC11A68108700A75B9A /* main.m in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
6ADAD6DE26493A420001F56E /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
6ADAD6E626493A420001F56E /* ShareViewController.m in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXSourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXTargetDependency section */
|
||||
6ADAD6EC26493A420001F56E /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
target = 6ADAD6E126493A420001F56E /* shareextension */;
|
||||
targetProxy = 6ADAD6EB26493A420001F56E /* PBXContainerItemProxy */;
|
||||
};
|
||||
/* End PBXTargetDependency section */
|
||||
|
||||
/* Begin PBXVariantGroup section */
|
||||
13B07FB11A68108700A75B9A /* LaunchScreen.xib */ = {
|
||||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
13B07FB21A68108700A75B9A /* Base */,
|
||||
);
|
||||
name = LaunchScreen.xib;
|
||||
path = multitarget;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
6ADAD6E726493A420001F56E /* MainInterface.storyboard */ = {
|
||||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
6ADAD6E826493A420001F56E /* Base */,
|
||||
);
|
||||
name = MainInterface.storyboard;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXVariantGroup section */
|
||||
|
||||
/* Begin XCBuildConfiguration section */
|
||||
13B07F941A680F5B00A75B9A /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 6C2E3173556A471DD304B334 /* Pods-multitarget.debug.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = QL76XYH73P;
|
||||
ENABLE_BITCODE = NO;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||
"$(inherited)",
|
||||
"FB_SONARKIT_ENABLED=1",
|
||||
);
|
||||
INFOPLIST_FILE = multitarget/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
"-ObjC",
|
||||
"-lc++",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.swmansion.dominik.multitarget.debug;
|
||||
PRODUCT_NAME = multitarget;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 5.0;
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
13B07F951A680F5B00A75B9A /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 7A4D352CD337FB3A3BF06240 /* Pods-multitarget.release.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
INFOPLIST_FILE = multitarget/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
"-ObjC",
|
||||
"-lc++",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.swmansion.dominik.multitarget;
|
||||
PRODUCT_NAME = multitarget;
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
SWIFT_VERSION = 5.0;
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
6ADAD6EF26493A420001F56E /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
|
||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
DEVELOPMENT_TEAM = QL76XYH73P;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
INFOPLIST_FILE = shareextension/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 14.5;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.swmansion.dominik.multitarget.shareextension;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SKIP_INSTALL = YES;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
6ADAD6F026493A420001F56E /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
|
||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_TEAM = QL76XYH73P;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
INFOPLIST_FILE = shareextension/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 14.5;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.swmansion.dominik.multitarget.shareextension;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SKIP_INSTALL = YES;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
83CBBA201A601CBA00E9B192 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_TESTABILITY = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu99;
|
||||
GCC_DYNAMIC_NO_PIC = NO;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_OPTIMIZATION_LEVEL = 0;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||
"DEBUG=1",
|
||||
"$(inherited)",
|
||||
);
|
||||
GCC_SYMBOLS_PRIVATE_EXTERN = NO;
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = "/usr/lib/swift $(inherited)";
|
||||
LIBRARY_SEARCH_PATHS = (
|
||||
"\"$(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME)\"",
|
||||
"\"$(TOOLCHAIN_DIR)/usr/lib/swift-5.0/$(PLATFORM_NAME)\"",
|
||||
"\"$(inherited)\"",
|
||||
);
|
||||
MTL_ENABLE_DEBUG_INFO = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
SDKROOT = iphoneos;
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
83CBBA211A601CBA00E9B192 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
COPY_PHASE_STRIP = YES;
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu99;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = "/usr/lib/swift $(inherited)";
|
||||
LIBRARY_SEARCH_PATHS = (
|
||||
"\"$(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME)\"",
|
||||
"\"$(TOOLCHAIN_DIR)/usr/lib/swift-5.0/$(PLATFORM_NAME)\"",
|
||||
"\"$(inherited)\"",
|
||||
);
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
SDKROOT = iphoneos;
|
||||
VALIDATE_PRODUCT = YES;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
/* End XCBuildConfiguration section */
|
||||
|
||||
/* Begin XCConfigurationList section */
|
||||
13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "multitarget" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
13B07F941A680F5B00A75B9A /* Debug */,
|
||||
13B07F951A680F5B00A75B9A /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
6ADAD6EE26493A420001F56E /* Build configuration list for PBXNativeTarget "shareextension" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
6ADAD6EF26493A420001F56E /* Debug */,
|
||||
6ADAD6F026493A420001F56E /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "multitarget" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
83CBBA201A601CBA00E9B192 /* Debug */,
|
||||
83CBBA211A601CBA00E9B192 /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
/* End XCConfigurationList section */
|
||||
};
|
||||
rootObject = 83CBB9F71A601CBA00E9B192 /* Project object */;
|
||||
}
|
||||
@@ -0,0 +1,492 @@
|
||||
// !$*UTF8*$!
|
||||
{
|
||||
archiveVersion = 1;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 46;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
13B07FBC1A68108700A75B9A /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB01A68108700A75B9A /* AppDelegate.m */; };
|
||||
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; };
|
||||
13B07FC11A68108700A75B9A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; };
|
||||
3E461D99554A48A4959DE609 /* SplashScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */; };
|
||||
96905EF65AED1B983A6B3ABC /* libPods-testapp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 58EEBF8E8E6FB1BC6CAF49B5 /* libPods-testapp.a */; };
|
||||
B18059E884C0ABDD17F3DC3D /* ExpoModulesProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAC715A2D49A985799AEE119 /* ExpoModulesProvider.swift */; };
|
||||
BB2F792D24A3F905000567C9 /* Expo.plist in Resources */ = {isa = PBXBuildFile; fileRef = BB2F792C24A3F905000567C9 /* Expo.plist */; };
|
||||
E112987CA504453081AC7CD8 /* noop-file.swift in Sources */ = {isa = PBXBuildFile; fileRef = C911AA20BDD841449D1CE041 /* noop-file.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
008F07F21AC5B25A0029DE68 /* main.jsbundle */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = main.jsbundle; sourceTree = "<group>"; };
|
||||
13B07F961A680F5B00A75B9A /* testapp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = testapp.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
13B07FAF1A68108700A75B9A /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = AppDelegate.h; path = testapp/AppDelegate.h; sourceTree = "<group>"; };
|
||||
13B07FB01A68108700A75B9A /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = AppDelegate.m; path = testapp/AppDelegate.m; sourceTree = "<group>"; };
|
||||
13B07FB51A68108700A75B9A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = testapp/Images.xcassets; sourceTree = "<group>"; };
|
||||
13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = testapp/Info.plist; sourceTree = "<group>"; };
|
||||
13B07FB71A68108700A75B9A /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = main.m; path = testapp/main.m; sourceTree = "<group>"; };
|
||||
58EEBF8E8E6FB1BC6CAF49B5 /* libPods-testapp.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-testapp.a"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
6C2E3173556A471DD304B334 /* Pods-testapp.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-testapp.debug.xcconfig"; path = "Target Support Files/Pods-testapp/Pods-testapp.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
7A4D352CD337FB3A3BF06240 /* Pods-testapp.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-testapp.release.xcconfig"; path = "Target Support Files/Pods-testapp/Pods-testapp.release.xcconfig"; sourceTree = "<group>"; };
|
||||
AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = SplashScreen.storyboard; path = testapp/SplashScreen.storyboard; sourceTree = "<group>"; };
|
||||
BB2F792C24A3F905000567C9 /* Expo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Expo.plist; sourceTree = "<group>"; };
|
||||
ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; };
|
||||
FAC715A2D49A985799AEE119 /* ExpoModulesProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ExpoModulesProvider.swift; path = "Pods/Target Support Files/Pods-testapp/ExpoModulesProvider.swift"; sourceTree = "<group>"; };
|
||||
C911AA20BDD841449D1CE041 /* noop-file.swift */ = {isa = PBXFileReference; name = "noop-file.swift"; path = "testapp/noop-file.swift"; sourceTree = "<group>"; fileEncoding = 4; lastKnownFileType = sourcecode.swift; explicitFileType = undefined; includeInIndex = 0; };
|
||||
0962172184234A4793928022 /* testapp-Bridging-Header.h */ = {isa = PBXFileReference; name = "testapp-Bridging-Header.h"; path = "testapp/testapp-Bridging-Header.h"; sourceTree = "<group>"; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; explicitFileType = undefined; includeInIndex = 0; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
13B07F8C1A680F5B00A75B9A /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
96905EF65AED1B983A6B3ABC /* libPods-testapp.a in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
13B07FAE1A68108700A75B9A /* testapp */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
BB2F792B24A3F905000567C9 /* Supporting */,
|
||||
008F07F21AC5B25A0029DE68 /* main.jsbundle */,
|
||||
13B07FAF1A68108700A75B9A /* AppDelegate.h */,
|
||||
13B07FB01A68108700A75B9A /* AppDelegate.m */,
|
||||
13B07FB51A68108700A75B9A /* Images.xcassets */,
|
||||
13B07FB61A68108700A75B9A /* Info.plist */,
|
||||
13B07FB71A68108700A75B9A /* main.m */,
|
||||
AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */,
|
||||
C911AA20BDD841449D1CE041 /* noop-file.swift */,
|
||||
0962172184234A4793928022 /* testapp-Bridging-Header.h */,
|
||||
);
|
||||
name = testapp;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
2D16E6871FA4F8E400B85C8A /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
ED297162215061F000B7C4FE /* JavaScriptCore.framework */,
|
||||
58EEBF8E8E6FB1BC6CAF49B5 /* libPods-testapp.a */,
|
||||
);
|
||||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
832341AE1AAA6A7D00B99B32 /* Libraries */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
);
|
||||
name = Libraries;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
83CBB9F61A601CBA00E9B192 = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
13B07FAE1A68108700A75B9A /* testapp */,
|
||||
832341AE1AAA6A7D00B99B32 /* Libraries */,
|
||||
83CBBA001A601CBA00E9B192 /* Products */,
|
||||
2D16E6871FA4F8E400B85C8A /* Frameworks */,
|
||||
D65327D7A22EEC0BE12398D9 /* Pods */,
|
||||
D7E4C46ADA2E9064B798F356 /* ExpoModulesProviders */,
|
||||
);
|
||||
indentWidth = 2;
|
||||
sourceTree = "<group>";
|
||||
tabWidth = 2;
|
||||
usesTabs = 0;
|
||||
};
|
||||
83CBBA001A601CBA00E9B192 /* Products */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
13B07F961A680F5B00A75B9A /* testapp.app */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
92DBD88DE9BF7D494EA9DA96 /* testapp */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
FAC715A2D49A985799AEE119 /* ExpoModulesProvider.swift */,
|
||||
);
|
||||
name = testapp;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
BB2F792B24A3F905000567C9 /* Supporting */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
BB2F792C24A3F905000567C9 /* Expo.plist */,
|
||||
);
|
||||
name = Supporting;
|
||||
path = testapp/Supporting;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D65327D7A22EEC0BE12398D9 /* Pods */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
6C2E3173556A471DD304B334 /* Pods-testapp.debug.xcconfig */,
|
||||
7A4D352CD337FB3A3BF06240 /* Pods-testapp.release.xcconfig */,
|
||||
);
|
||||
path = Pods;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D7E4C46ADA2E9064B798F356 /* ExpoModulesProviders */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
92DBD88DE9BF7D494EA9DA96 /* testapp */,
|
||||
);
|
||||
name = ExpoModulesProviders;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
13B07F861A680F5B00A75B9A /* testapp */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "testapp" */;
|
||||
buildPhases = (
|
||||
08A4A3CD28434E44B6B9DE2E /* [CP] Check Pods Manifest.lock */,
|
||||
FD10A7F022414F080027D42C /* Start Packager */,
|
||||
13B07F871A680F5B00A75B9A /* Sources */,
|
||||
13B07F8C1A680F5B00A75B9A /* Frameworks */,
|
||||
13B07F8E1A680F5B00A75B9A /* Resources */,
|
||||
00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */,
|
||||
800E24972A6A228C8D4807E9 /* [CP] Copy Pods Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
);
|
||||
name = testapp;
|
||||
productName = testapp;
|
||||
productReference = 13B07F961A680F5B00A75B9A /* testapp.app */;
|
||||
productType = "com.apple.product-type.application";
|
||||
};
|
||||
/* End PBXNativeTarget section */
|
||||
|
||||
/* Begin PBXProject section */
|
||||
83CBB9F71A601CBA00E9B192 /* Project object */ = {
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
LastUpgradeCheck = 1130;
|
||||
TargetAttributes = {
|
||||
13B07F861A680F5B00A75B9A = {
|
||||
LastSwiftMigration = 1250;
|
||||
};
|
||||
};
|
||||
};
|
||||
buildConfigurationList = 83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "testapp" */;
|
||||
compatibilityVersion = "Xcode 3.2";
|
||||
developmentRegion = en;
|
||||
hasScannedForEncodings = 0;
|
||||
knownRegions = (
|
||||
en,
|
||||
Base,
|
||||
);
|
||||
mainGroup = 83CBB9F61A601CBA00E9B192;
|
||||
productRefGroup = 83CBBA001A601CBA00E9B192 /* Products */;
|
||||
projectDirPath = "";
|
||||
projectRoot = "";
|
||||
targets = (
|
||||
13B07F861A680F5B00A75B9A /* testapp */,
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
|
||||
/* Begin PBXResourcesBuildPhase section */
|
||||
13B07F8E1A680F5B00A75B9A /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
BB2F792D24A3F905000567C9 /* Expo.plist in Resources */,
|
||||
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */,
|
||||
3E461D99554A48A4959DE609 /* SplashScreen.storyboard in Resources */,
|
||||
C9A08D1C39C94E5D824DFA3F /* testapp-Bridging-Header.h in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXResourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXShellScriptBuildPhase section */
|
||||
00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputPaths = (
|
||||
);
|
||||
name = "Bundle React Native code and images";
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "export NODE_BINARY=node\n\n# The project root by default is one level up from the ios directory\nexport PROJECT_ROOT=\"$PROJECT_DIR\"/..\n\n`node --print \"require('path').dirname(require.resolve('react-native/package.json')) + '/scripts/react-native-xcode.sh'\"`\n";
|
||||
};
|
||||
08A4A3CD28434E44B6B9DE2E /* [CP] Check Pods Manifest.lock */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
);
|
||||
inputPaths = (
|
||||
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
|
||||
"${PODS_ROOT}/Manifest.lock",
|
||||
);
|
||||
name = "[CP] Check Pods Manifest.lock";
|
||||
outputFileListPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
"$(DERIVED_FILE_DIR)/Pods-testapp-checkManifestLockResult.txt",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
800E24972A6A228C8D4807E9 /* [CP] Copy Pods Resources */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-testapp/Pods-testapp-resources.sh",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/EXConstants/EXConstants.bundle",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/EXUpdates/EXUpdates.bundle",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/React-Core/AccessibilityResources.bundle",
|
||||
);
|
||||
name = "[CP] Copy Pods Resources";
|
||||
outputPaths = (
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/EXConstants.bundle",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/EXUpdates.bundle",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/AccessibilityResources.bundle",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-testapp/Pods-testapp-resources.sh\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
FD10A7F022414F080027D42C /* Start Packager */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
);
|
||||
inputPaths = (
|
||||
);
|
||||
name = "Start Packager";
|
||||
outputFileListPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "export RCT_METRO_PORT=\"${RCT_METRO_PORT:=8081}\"\necho \"export RCT_METRO_PORT=${RCT_METRO_PORT}\" > `node --print \"require('path').dirname(require.resolve('react-native/package.json')) + '/scripts/.packager.env'\"`\nif [ -z \"${RCT_NO_LAUNCH_PACKAGER+xxx}\" ] ; then\n if nc -w 5 -z localhost ${RCT_METRO_PORT} ; then\n if ! curl -s \"http://localhost:${RCT_METRO_PORT}/status\" | grep -q \"packager-status:running\" ; then\n echo \"Port ${RCT_METRO_PORT} already in use, packager is either not running or not running correctly\"\n exit 2\n fi\n else\n open `node --print \"require('path').dirname(require.resolve('react-native/package.json')) + '/scripts/launchPackager.command'\"` || echo \"Can't start packager automatically\"\n fi\nfi\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
/* End PBXShellScriptBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
13B07F871A680F5B00A75B9A /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
13B07FBC1A68108700A75B9A /* AppDelegate.m in Sources */,
|
||||
13B07FC11A68108700A75B9A /* main.m in Sources */,
|
||||
B18059E884C0ABDD17F3DC3D /* ExpoModulesProvider.swift in Sources */,
|
||||
E112987CA504453081AC7CD8 /* noop-file.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXSourcesBuildPhase section */
|
||||
|
||||
/* Begin XCBuildConfiguration section */
|
||||
13B07F941A680F5B00A75B9A /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 6C2E3173556A471DD304B334 /* Pods-testapp.debug.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
ENABLE_BITCODE = NO;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||
"$(inherited)",
|
||||
"FB_SONARKIT_ENABLED=1",
|
||||
);
|
||||
INFOPLIST_FILE = testapp/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
"-ObjC",
|
||||
"-lc++",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "org.reactjs.native.example.testapp.turtlev2";
|
||||
PRODUCT_NAME = turtlev2;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 5.0;
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = testapp/testapp-Bridging-Header.h;
|
||||
CODE_SIGN_ENTITLEMENTS = testapp/turtlev2.entitlements;
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
13B07F951A680F5B00A75B9A /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 7A4D352CD337FB3A3BF06240 /* Pods-testapp.release.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
INFOPLIST_FILE = testapp/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
"-ObjC",
|
||||
"-lc++",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "org.reactjs.native.example.testapp.turtlev2";
|
||||
PRODUCT_NAME = turtlev2;
|
||||
SWIFT_VERSION = 5.0;
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = testapp/testapp-Bridging-Header.h;
|
||||
CODE_SIGN_ENTITLEMENTS = testapp/turtlev2.entitlements;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
83CBBA201A601CBA00E9B192 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_TESTABILITY = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu99;
|
||||
GCC_DYNAMIC_NO_PIC = NO;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_OPTIMIZATION_LEVEL = 0;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||
"DEBUG=1",
|
||||
"$(inherited)",
|
||||
);
|
||||
GCC_SYMBOLS_PRIVATE_EXTERN = NO;
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = "/usr/lib/swift $(inherited)";
|
||||
LIBRARY_SEARCH_PATHS = "\"$(inherited)\"";
|
||||
MTL_ENABLE_DEBUG_INFO = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
SDKROOT = iphoneos;
|
||||
CODE_SIGN_ENTITLEMENTS = testapp/turtlev2.entitlements;
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
83CBBA211A601CBA00E9B192 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
COPY_PHASE_STRIP = YES;
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu99;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = "/usr/lib/swift $(inherited)";
|
||||
LIBRARY_SEARCH_PATHS = "\"$(inherited)\"";
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
SDKROOT = iphoneos;
|
||||
VALIDATE_PRODUCT = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = testapp/turtlev2.entitlements;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
/* End XCBuildConfiguration section */
|
||||
|
||||
/* Begin XCConfigurationList section */
|
||||
13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "testapp" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
13B07F941A680F5B00A75B9A /* Debug */,
|
||||
13B07F951A680F5B00A75B9A /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "testapp" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
83CBBA201A601CBA00E9B192 /* Debug */,
|
||||
83CBBA211A601CBA00E9B192 /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
/* End XCConfigurationList section */
|
||||
};
|
||||
rootObject = 83CBB9F71A601CBA00E9B192 /* Project object */;
|
||||
}
|
||||
@@ -0,0 +1,255 @@
|
||||
import { vol } from 'memfs';
|
||||
|
||||
import { Credentials } from '../credentials/manager';
|
||||
import { DistributionType } from '../credentials/provisioningProfile';
|
||||
import { createGymfileForArchiveBuild, createGymfileForSimulatorBuild } from '../gymfile';
|
||||
|
||||
describe('gymfile', () => {
|
||||
beforeEach(() => {
|
||||
vol.reset();
|
||||
// Set up /tmp directory in the mock filesystem
|
||||
vol.fromJSON({
|
||||
'/tmp/.keep': '', // Create /tmp directory
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vol.reset();
|
||||
});
|
||||
|
||||
describe('createGymfileForArchiveBuild', () => {
|
||||
it('should create Gymfile with all variables substituted correctly', async () => {
|
||||
const mockCredentials: Credentials = {
|
||||
keychainPath: '/Users/expo/Library/Keychains/login.keychain',
|
||||
distributionType: DistributionType.APP_STORE,
|
||||
teamId: 'TEAM123',
|
||||
applicationTargetProvisioningProfile: {} as any,
|
||||
targetProvisioningProfiles: {
|
||||
'com.example.app': {
|
||||
bundleIdentifier: 'com.example.app',
|
||||
uuid: 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee',
|
||||
path: '/path/to/profile1.mobileprovision',
|
||||
target: 'com.example.app',
|
||||
teamId: 'TEAM123',
|
||||
name: 'Main App Profile',
|
||||
developerCertificate: Buffer.from('cert'),
|
||||
certificateCommonName: 'iPhone Distribution',
|
||||
distributionType: DistributionType.APP_STORE,
|
||||
},
|
||||
'com.example.app.widget': {
|
||||
bundleIdentifier: 'com.example.app.widget',
|
||||
uuid: 'ffffffff-0000-1111-2222-333333333333',
|
||||
path: '/path/to/profile2.mobileprovision',
|
||||
target: 'com.example.app.widget',
|
||||
teamId: 'TEAM123',
|
||||
name: 'Widget Profile',
|
||||
developerCertificate: Buffer.from('cert'),
|
||||
certificateCommonName: 'iPhone Distribution',
|
||||
distributionType: DistributionType.APP_STORE,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const outputFile = '/tmp/Gymfile';
|
||||
|
||||
await createGymfileForArchiveBuild({
|
||||
outputFile,
|
||||
credentials: mockCredentials,
|
||||
scheme: 'MyApp',
|
||||
buildConfiguration: 'Release',
|
||||
outputDirectory: '/tmp/output',
|
||||
clean: true,
|
||||
logsDirectory: '/tmp/logs',
|
||||
});
|
||||
|
||||
const generatedContent = vol.readFileSync(outputFile, 'utf-8') as string;
|
||||
expect(generatedContent).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should create Gymfile without build configuration when not provided', async () => {
|
||||
const mockCredentials: Credentials = {
|
||||
keychainPath: '/tmp/keychain',
|
||||
distributionType: DistributionType.AD_HOC,
|
||||
teamId: 'TEAM123',
|
||||
applicationTargetProvisioningProfile: {} as any,
|
||||
targetProvisioningProfiles: {
|
||||
'com.example.app': {
|
||||
bundleIdentifier: 'com.example.app',
|
||||
uuid: 'test-uuid',
|
||||
path: '/path/to/profile.mobileprovision',
|
||||
target: 'com.example.app',
|
||||
teamId: 'TEAM123',
|
||||
name: 'Profile',
|
||||
developerCertificate: Buffer.from('cert'),
|
||||
certificateCommonName: 'iPhone Distribution',
|
||||
distributionType: DistributionType.AD_HOC,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const outputFile = '/tmp/Gymfile';
|
||||
|
||||
await createGymfileForArchiveBuild({
|
||||
outputFile,
|
||||
credentials: mockCredentials,
|
||||
scheme: 'TestScheme',
|
||||
outputDirectory: '/tmp/output',
|
||||
clean: false,
|
||||
logsDirectory: '/tmp/logs',
|
||||
});
|
||||
|
||||
const generatedContent = vol.readFileSync(outputFile, 'utf-8') as string;
|
||||
expect(generatedContent).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should include iCloud container environment when provided in entitlements', async () => {
|
||||
const mockCredentials: Credentials = {
|
||||
keychainPath: '/tmp/keychain',
|
||||
distributionType: DistributionType.APP_STORE,
|
||||
teamId: 'TEAM123',
|
||||
applicationTargetProvisioningProfile: {} as any,
|
||||
targetProvisioningProfiles: {
|
||||
'com.example.app': {
|
||||
bundleIdentifier: 'com.example.app',
|
||||
uuid: 'test-uuid',
|
||||
path: '/path/to/profile.mobileprovision',
|
||||
target: 'com.example.app',
|
||||
teamId: 'TEAM123',
|
||||
name: 'Profile',
|
||||
developerCertificate: Buffer.from('cert'),
|
||||
certificateCommonName: 'iPhone Distribution',
|
||||
distributionType: DistributionType.APP_STORE,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const outputFile = '/tmp/Gymfile';
|
||||
|
||||
await createGymfileForArchiveBuild({
|
||||
outputFile,
|
||||
credentials: mockCredentials,
|
||||
scheme: 'MyApp',
|
||||
buildConfiguration: 'Release',
|
||||
outputDirectory: '/tmp/output',
|
||||
clean: true,
|
||||
logsDirectory: '/tmp/logs',
|
||||
entitlements: {
|
||||
'com.apple.developer.icloud-container-environment': 'Production',
|
||||
},
|
||||
});
|
||||
|
||||
const generatedContent = vol.readFileSync(outputFile, 'utf-8') as string;
|
||||
expect(generatedContent).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should handle multiple provisioning profiles correctly', async () => {
|
||||
const mockCredentials: Credentials = {
|
||||
keychainPath: '/tmp/keychain',
|
||||
distributionType: DistributionType.ENTERPRISE,
|
||||
teamId: 'TEAM123',
|
||||
applicationTargetProvisioningProfile: {} as any,
|
||||
targetProvisioningProfiles: {
|
||||
'com.example.app': {
|
||||
bundleIdentifier: 'com.example.app',
|
||||
uuid: 'main-app-uuid',
|
||||
path: '/path/to/main.mobileprovision',
|
||||
target: 'com.example.app',
|
||||
teamId: 'TEAM123',
|
||||
name: 'Main Profile',
|
||||
developerCertificate: Buffer.from('cert'),
|
||||
certificateCommonName: 'iPhone Distribution',
|
||||
distributionType: DistributionType.ENTERPRISE,
|
||||
},
|
||||
'com.example.app.widget': {
|
||||
bundleIdentifier: 'com.example.app.widget',
|
||||
uuid: 'widget-uuid',
|
||||
path: '/path/to/widget.mobileprovision',
|
||||
target: 'com.example.app.widget',
|
||||
teamId: 'TEAM123',
|
||||
name: 'Widget Profile',
|
||||
developerCertificate: Buffer.from('cert'),
|
||||
certificateCommonName: 'iPhone Distribution',
|
||||
distributionType: DistributionType.ENTERPRISE,
|
||||
},
|
||||
'com.example.app.extension': {
|
||||
bundleIdentifier: 'com.example.app.extension',
|
||||
uuid: 'extension-uuid',
|
||||
path: '/path/to/extension.mobileprovision',
|
||||
target: 'com.example.app.extension',
|
||||
teamId: 'TEAM123',
|
||||
name: 'Extension Profile',
|
||||
developerCertificate: Buffer.from('cert'),
|
||||
certificateCommonName: 'iPhone Distribution',
|
||||
distributionType: DistributionType.ENTERPRISE,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const outputFile = '/tmp/Gymfile';
|
||||
|
||||
await createGymfileForArchiveBuild({
|
||||
outputFile,
|
||||
credentials: mockCredentials,
|
||||
scheme: 'MyApp',
|
||||
outputDirectory: '/tmp/output',
|
||||
clean: true,
|
||||
logsDirectory: '/tmp/logs',
|
||||
});
|
||||
|
||||
const generatedContent = vol.readFileSync(outputFile, 'utf-8') as string;
|
||||
expect(generatedContent).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe('createGymfileForSimulatorBuild', () => {
|
||||
it('should create Gymfile with all simulator variables substituted correctly', async () => {
|
||||
const outputFile = '/tmp/Gymfile';
|
||||
|
||||
await createGymfileForSimulatorBuild({
|
||||
outputFile,
|
||||
scheme: 'MyApp',
|
||||
buildConfiguration: 'Debug',
|
||||
derivedDataPath: '/tmp/derived-data',
|
||||
clean: true,
|
||||
logsDirectory: '/tmp/logs',
|
||||
simulatorDestination: 'generic/platform=iOS Simulator',
|
||||
});
|
||||
|
||||
const generatedContent = vol.readFileSync(outputFile, 'utf-8') as string;
|
||||
expect(generatedContent).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should create Gymfile without configuration when not provided', async () => {
|
||||
const outputFile = '/tmp/Gymfile';
|
||||
|
||||
await createGymfileForSimulatorBuild({
|
||||
outputFile,
|
||||
scheme: 'TestApp',
|
||||
derivedDataPath: '/tmp/derived',
|
||||
clean: false,
|
||||
logsDirectory: '/tmp/logs',
|
||||
simulatorDestination: 'platform=iOS Simulator,name=iPhone 15',
|
||||
});
|
||||
|
||||
const generatedContent = vol.readFileSync(outputFile, 'utf-8') as string;
|
||||
expect(generatedContent).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should handle tvOS simulator destination', async () => {
|
||||
const outputFile = '/tmp/Gymfile';
|
||||
|
||||
await createGymfileForSimulatorBuild({
|
||||
outputFile,
|
||||
scheme: 'MyTVApp',
|
||||
buildConfiguration: 'Debug',
|
||||
derivedDataPath: '/tmp/derived',
|
||||
clean: true,
|
||||
logsDirectory: '/tmp/logs',
|
||||
simulatorDestination: 'generic/platform=tvOS Simulator',
|
||||
});
|
||||
|
||||
const generatedContent = vol.readFileSync(outputFile, 'utf-8') as string;
|
||||
expect(generatedContent).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,39 @@
|
||||
import { Ios } from '@expo/eas-build-job';
|
||||
import { bunyan } from '@expo/logger';
|
||||
import fs from 'fs-extra';
|
||||
import { vol } from 'memfs';
|
||||
import { instance, mock, verify, when } from 'ts-mockito';
|
||||
|
||||
import { BuildContext } from '../../context';
|
||||
import { deleteXcodeEnvLocalIfExistsAsync } from '../xcodeEnv';
|
||||
|
||||
jest.mock('fs');
|
||||
|
||||
afterEach(() => {
|
||||
vol.reset();
|
||||
});
|
||||
|
||||
describe(deleteXcodeEnvLocalIfExistsAsync, () => {
|
||||
it('removes ios/.xcode.env.local if exists + calls ctx.markBuildPhaseHasWarnings', async () => {
|
||||
vol.fromJSON(
|
||||
{
|
||||
'ios/.xcode.env': '# lorem ipsum',
|
||||
'ios/.xcode.env.local': '# lorem ipsum',
|
||||
},
|
||||
'/app'
|
||||
);
|
||||
|
||||
const mockCtx = mock<BuildContext<Ios.Job>>();
|
||||
when(mockCtx.getReactNativeProjectDirectory()).thenReturn('/app');
|
||||
when(mockCtx.logger).thenReturn(instance(mock<bunyan>()));
|
||||
const ctx = instance(mockCtx);
|
||||
|
||||
await deleteXcodeEnvLocalIfExistsAsync(ctx);
|
||||
|
||||
await expect(fs.pathExists('/app/ios/.xcode.env')).resolves.toBe(true);
|
||||
await expect(fs.pathExists('/app/ios/.xcode.env.local')).resolves.toBe(false);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-confusing-void-expression
|
||||
verify(mockCtx.markBuildPhaseHasWarnings()).called();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,128 @@
|
||||
import { IOSConfig } from '@expo/config-plugins';
|
||||
import { Ios } from '@expo/eas-build-job';
|
||||
import plist from '@expo/plist';
|
||||
import fs from 'fs-extra';
|
||||
import uniq from 'lodash/uniq';
|
||||
import path from 'path';
|
||||
|
||||
import { Credentials } from './credentials/manager';
|
||||
import { BuildContext } from '../context';
|
||||
|
||||
async function configureXcodeProject(
|
||||
ctx: BuildContext<Ios.Job>,
|
||||
{
|
||||
credentials,
|
||||
buildConfiguration,
|
||||
}: {
|
||||
credentials: Credentials;
|
||||
buildConfiguration: string;
|
||||
}
|
||||
): Promise<void> {
|
||||
ctx.logger.info('Configuring Xcode project');
|
||||
await configureCredentialsAsync(ctx, {
|
||||
credentials,
|
||||
buildConfiguration,
|
||||
});
|
||||
const { version } = ctx.job;
|
||||
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
|
||||
if (version?.appVersion || version?.buildNumber) {
|
||||
await updateVersionsAsync(ctx, {
|
||||
targetNames: Object.keys(credentials.targetProvisioningProfiles),
|
||||
buildConfiguration,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function configureCredentialsAsync(
|
||||
ctx: BuildContext<Ios.Job>,
|
||||
{
|
||||
credentials,
|
||||
buildConfiguration,
|
||||
}: {
|
||||
credentials: Credentials;
|
||||
buildConfiguration: string;
|
||||
}
|
||||
): Promise<void> {
|
||||
const targetNames = Object.keys(credentials.targetProvisioningProfiles);
|
||||
for (const targetName of targetNames) {
|
||||
const profile = credentials.targetProvisioningProfiles[targetName];
|
||||
ctx.logger.info(
|
||||
`Assigning provisioning profile '${profile.name}' (Apple Team ID: ${profile.teamId}) to target '${targetName}'`
|
||||
);
|
||||
IOSConfig.ProvisioningProfile.setProvisioningProfileForPbxproj(
|
||||
ctx.getReactNativeProjectDirectory(),
|
||||
{
|
||||
targetName,
|
||||
profileName: profile.name,
|
||||
appleTeamId: profile.teamId,
|
||||
buildConfiguration,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function updateVersionsAsync(
|
||||
ctx: BuildContext<Ios.Job>,
|
||||
{
|
||||
targetNames,
|
||||
buildConfiguration,
|
||||
}: {
|
||||
targetNames: string[];
|
||||
buildConfiguration: string;
|
||||
}
|
||||
): Promise<void> {
|
||||
const project = IOSConfig.XcodeUtils.getPbxproj(ctx.getReactNativeProjectDirectory());
|
||||
const iosDir = path.join(ctx.getReactNativeProjectDirectory(), 'ios');
|
||||
|
||||
const infoPlistPaths: string[] = [];
|
||||
for (const targetName of targetNames) {
|
||||
const xcBuildConfiguration = IOSConfig.Target.getXCBuildConfigurationFromPbxproj(project, {
|
||||
targetName,
|
||||
buildConfiguration,
|
||||
});
|
||||
const infoPlist = xcBuildConfiguration.buildSettings.INFOPLIST_FILE;
|
||||
if (infoPlist) {
|
||||
const evaluatedInfoPlistPath = trimQuotes(
|
||||
evaluateTemplateString(infoPlist, {
|
||||
SRCROOT: iosDir,
|
||||
})
|
||||
);
|
||||
const absolutePath = path.isAbsolute(evaluatedInfoPlistPath)
|
||||
? evaluatedInfoPlistPath
|
||||
: path.join(iosDir, evaluatedInfoPlistPath);
|
||||
infoPlistPaths.push(path.normalize(absolutePath));
|
||||
}
|
||||
}
|
||||
const uniqueInfoPlistPaths = uniq(infoPlistPaths);
|
||||
for (const infoPlistPath of uniqueInfoPlistPaths) {
|
||||
ctx.logger.info(`Updating versions in ${infoPlistPath}`);
|
||||
const infoPlistRaw = await fs.readFile(infoPlistPath, 'utf-8');
|
||||
const infoPlist = plist.parse(infoPlistRaw) as IOSConfig.InfoPlist;
|
||||
if (ctx.job.version?.buildNumber) {
|
||||
infoPlist.CFBundleVersion = ctx.job.version?.buildNumber;
|
||||
}
|
||||
if (ctx.job.version?.appVersion) {
|
||||
infoPlist.CFBundleShortVersionString = ctx.job.version?.appVersion;
|
||||
}
|
||||
await fs.writeFile(infoPlistPath, plist.build(infoPlist));
|
||||
}
|
||||
}
|
||||
|
||||
function trimQuotes(s: string): string {
|
||||
return s?.startsWith('"') && s.endsWith('"') ? s.slice(1, -1) : s;
|
||||
}
|
||||
|
||||
export function evaluateTemplateString(s: string, buildSettings: Record<string, string>): string {
|
||||
// necessary because buildSettings might be XCBuildConfiguration['buildSettings'] which is not a plain object
|
||||
const vars = { ...buildSettings };
|
||||
return s.replace(/\$\((\w+)\)/g, (match, key) => {
|
||||
if (vars.hasOwnProperty(key)) {
|
||||
const value = String(vars[key]);
|
||||
return trimQuotes(value);
|
||||
} else {
|
||||
return match;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export { configureXcodeProject };
|
||||
Loaded 100 of 856 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user