-
Notifications
You must be signed in to change notification settings - Fork 132
/
Copy pathbuild_testapps.py
716 lines (592 loc) · 25.5 KB
/
build_testapps.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
# Copyright 2022 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
r"""Build automation tool for Firebase C++ testapps for desktop and mobile.
USAGE:
This tool has a number of dependencies (listed below). Once those are taken
care of, here is an example of an execution of the tool (on MacOS):
python build_testapps.py --t auth,messaging --p iOS --s /tmp/firebase-cpp-sdk
Critical flags:
--t (full name: testapps, default: None)
--p (full name: platforms, default: None)
--s (full name: packaged_sdk, default: None)
By default, this tool will build integration tests from source, which involves
Under most circumstances the other flags don't need to be set, but can be
seen by running --help. Note that all path flags will forcefully expand
the user ~.
DEPENDENCIES:
----Firebase Repo----
The Firebase C++ Quickstart repo must be locally present.
Path specified by the flag:
--repo_dir (default: current working directory)
----Python Dependencies----
The requirements.txt file has the required dependencies for this Python tool.
pip install -r requirements.txt
----CMake (Desktop only)----
CMake must be installed and on the system path.
----Environment Variables (Android only)----
If building for Android, gradle requires several environment variables.
The following lists expected variables, and examples of what
a configured value may look like on MacOS:
JAVA_HOME=/Library/Java/JavaVirtualMachines/jdk-8-latest/Contents/Home
ANDROID_HOME=/Users/user_name/Library/Android/sdk
ANDROID_SDK_HOME=/Users/user_name/Library/Android/sdk
ANDROID_NDK_HOME=/Users/user_name/Library/Android/sdk/ndk-bundle
Or on Linux:
JAVA_HOME=/usr/local/buildtools/java/jdk/
ANDROID_HOME=~/Android/Sdk
ANDROID_SDK_HOME=~/Android/Sdk
ANDROID_NDK_HOME=~/Android/Sdk/ndk
If using this tool frequently, you will likely find it convenient to
modify your bashrc file to automatically set these variables.
"""
import attr
import datetime
import json
import os
import platform
import shutil
import stat
import subprocess
import sys
import tempfile
from absl import app
from absl import flags
from absl import logging
import utils
import config_reader
import xcodebuild
# Environment variables
_JAVA_HOME = "JAVA_HOME"
_ANDROID_HOME = "ANDROID_HOME"
_ANDROID_SDK_HOME = "ANDROID_SDK_HOME"
_NDK_ROOT = "NDK_ROOT"
_ANDROID_NDK_HOME = "ANDROID_NDK_HOME"
# Platforms
_ANDROID = "Android"
_IOS = "iOS"
_TVOS = "tvOS"
_DESKTOP = "Desktop"
_SUPPORTED_PLATFORMS = (_ANDROID, _IOS, _TVOS, _DESKTOP)
# Architecture
_SUPPORTED_ARCHITECTURES = ("x64", "x86", "arm64")
# Values for iOS SDK flag (where the iOS app will run)
_APPLE_SDK_DEVICE = "real"
_APPLE_SDK_SIMULATOR = "virtual"
_SUPPORTED_APPLE_SDK = (_APPLE_SDK_DEVICE, _APPLE_SDK_SIMULATOR)
_DEFAULT_RUN_TIMEOUT_SECONDS = 4800 # 1 hour 20 min
FLAGS = flags.FLAGS
flags.DEFINE_string(
"packaged_sdk", None, "Firebase SDK directory.")
flags.DEFINE_string(
"output_directory", "~",
"Build output will be placed in this directory.")
flags.DEFINE_string(
"artifact_name", "local-build",
"artifacts will be created and placed in output_directory."
" testapps artifact is testapps-$artifact_name;"
" build log artifact is build-results-$artifact_name.log.")
flags.DEFINE_string(
"repo_dir", os.getcwd(),
"Firebase C++ Quickstart Git repository. Current directory by default.")
flags.DEFINE_list(
"testapps", None, "Which testapps (Firebase APIs) to build, e.g."
" 'analytics,auth'.",
short_name="t")
flags.DEFINE_list(
"platforms", None, "Which platforms to build. Can be Android, iOS and/or"
" Desktop", short_name="p")
flags.DEFINE_bool(
"add_timestamp", True,
"Add a timestamp to the output directory for disambiguation."
" Recommended when running locally, so each execution gets its own "
" directory.")
flags.DEFINE_list(
"ios_sdk", _APPLE_SDK_DEVICE,
"(iOS only) Build for real device (.ipa), virtual device / simulator (.app), "
"or both. Building for both will produce both an .app and an .ipa.")
flags.DEFINE_list(
"tvos_sdk", _APPLE_SDK_SIMULATOR,
"(tvOS only) Build for real device (.ipa), virtual device / simulator (.app), "
"or both. Building for both will produce both an .app and an .ipa.")
flags.DEFINE_bool(
"update_pod_repo", True,
"(iOS/tvOS only) Will run 'pod repo update' before building for iOS/tvOS to update"
" the local spec repos available on this machine. Must also include iOS/tvOS"
" in platforms flag.")
flags.DEFINE_string(
"compiler", None,
"(Desktop only) Specify the compiler with CMake during the testapps build."
" Check the config file to see valid choices for this flag."
" If none, will invoke cmake without specifying a compiler.")
flags.DEFINE_string(
"arch", "x64",
"(Desktop only) Which architecture to build: x64 (all), x86 (Windows/Linux), "
"or arm64 (Mac only).")
# Get the number of CPUs for the default value of FLAGS.jobs
CPU_COUNT = os.cpu_count();
# If CPU count couldn't be determined, default to 2.
DEFAULT_CPU_COUNT = 2
if CPU_COUNT is None: CPU_COUNT = DEFAULT_CPU_COUNT
# Cap at 4 CPUs.
MAX_CPU_COUNT = 4
if CPU_COUNT > MAX_CPU_COUNT: CPU_COUNT = MAX_CPU_COUNT
flags.DEFINE_integer(
"jobs", CPU_COUNT,
"(Desktop only) If > 0, pass in -j <number> to make CMake parallelize the"
" build. Defaults to the system's CPU count (max %s)." % MAX_CPU_COUNT)
flags.DEFINE_multi_string(
"cmake_flag", None,
"Pass an additional flag to the CMake configure step."
" This option can be specified multiple times.")
flags.register_validator(
"platforms",
lambda p: all(platform in _SUPPORTED_PLATFORMS for platform in p),
message="Valid platforms: " + ",".join(_SUPPORTED_PLATFORMS),
flag_values=FLAGS)
flags.register_validator(
"ios_sdk",
lambda s: all(ios_sdk in _SUPPORTED_APPLE_SDK for ios_sdk in s),
message="Valid platforms: " + ",".join(_SUPPORTED_APPLE_SDK),
flag_values=FLAGS)
flags.register_validator(
"tvos_sdk",
lambda s: all(tvos_sdk in _SUPPORTED_APPLE_SDK for tvos_sdk in s),
message="Valid platforms: " + ",".join(_SUPPORTED_APPLE_SDK),
flag_values=FLAGS)
flags.DEFINE_bool(
"short_output_paths", False,
"Use short directory names for output paths. Useful to avoid hitting file "
"path limits on Windows.")
flags.DEFINE_bool(
"gha_build", False,
"Set to true if this is a GitHub Actions build.")
def main(argv):
if len(argv) > 1:
raise app.UsageError("Too many command-line arguments.")
platforms = FLAGS.platforms
testapps = FLAGS.testapps
sdk_dir = _fix_path(FLAGS.packaged_sdk)
root_output_dir = _fix_path(FLAGS.output_directory)
repo_dir = _fix_path(FLAGS.repo_dir)
update_pod_repo = FLAGS.update_pod_repo
if FLAGS.add_timestamp:
timestamp = datetime.datetime.now().strftime("%Y_%m_%d-%H_%M_%S")
else:
timestamp = ""
if FLAGS.short_output_paths:
output_dir = os.path.join(root_output_dir, "ta")
else:
output_dir = os.path.join(root_output_dir, "testapps" + timestamp)
config = config_reader.read_config()
xcframework_dir = os.path.join(sdk_dir, "xcframeworks")
xcframework_exist = os.path.isdir(xcframework_dir)
if not xcframework_exist:
if _IOS in platforms:
_build_xcframework_from_repo(repo_dir, "ios", testapps, config)
if _TVOS in platforms:
_build_xcframework_from_repo(repo_dir, "tvos", testapps, config)
if update_pod_repo and (_IOS in platforms or _TVOS in platforms):
_run(["pod", "repo", "update"])
cmake_flags = _get_desktop_compiler_flags(FLAGS.compiler, config.compilers)
if (_DESKTOP in platforms and utils.is_linux_os() and FLAGS.arch == "x86"):
# Write out a temporary toolchain file to force 32-bit Linux builds, as
# the SDK-included toolchain file may not be present when building against
# the packaged SDK.
temp_toolchain_file = tempfile.NamedTemporaryFile("w+", suffix=".cmake")
temp_toolchain_file.writelines([
'set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -m32")\n',
'set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -m32")\n',
'set(CMAKE_LIBRARY_PATH "/usr/lib/i386-linux-gnu")\n',
'set(INCLUDE_DIRECTORIES ${INCLUDE_DIRECTORIES} "/usr/include/i386-linux-gnu")\n'])
temp_toolchain_file.flush()
# Leave the file open, as it will be deleted on close, i.e. when this script exits.
# (On Linux, the file can be opened a second time by cmake while still open by
# this script)
cmake_flags.extend(["-DCMAKE_TOOLCHAIN_FILE=%s" % temp_toolchain_file.name])
if FLAGS.cmake_flag:
cmake_flags.extend(FLAGS.cmake_flag)
failures = []
for testapp in testapps:
api_config = config.get_api(testapp)
testapp_dirs = [api_config.testapp_path]
for testapp_dir in testapp_dirs:
logging.info("BEGIN building for %s: %s", testapp, testapp_dir)
failures += _build(
testapp=testapp,
platforms=platforms,
api_config=config.get_api(testapp),
testapp_dir=testapp_dir,
output_dir=output_dir,
sdk_dir=sdk_dir,
xcframework_exist=xcframework_exist,
repo_dir=repo_dir,
ios_sdk=FLAGS.ios_sdk,
tvos_sdk=FLAGS.tvos_sdk,
cmake_flags=cmake_flags,
short_output_paths=FLAGS.short_output_paths)
logging.info("END building for %s", testapp)
_collect_integration_tests(testapps, root_output_dir, output_dir, FLAGS.artifact_name)
_summarize_results(testapps, platforms, failures, root_output_dir, FLAGS.artifact_name)
return 1 if failures else 0
def _build(
testapp, platforms, api_config, testapp_dir, output_dir, sdk_dir, xcframework_exist,
repo_dir, ios_sdk, tvos_sdk, cmake_flags, short_output_paths):
"""Builds one testapp on each of the specified platforms."""
os.chdir(repo_dir)
project_dir = os.path.join(output_dir, api_config.name)
if short_output_paths:
# Combining the first letter of every part separated by underscore for
# testapp paths. This is a trick to reduce file path length as we were
# exceeding the limit on Windows.
testapp_dir_parts = os.path.basename(testapp_dir).split('_')
output_testapp_dir = ''.join([x[0] for x in testapp_dir_parts])
else:
output_testapp_dir = os.path.basename(testapp_dir)
project_dir = os.path.join(project_dir, output_testapp_dir)
logging.info("Copying testapp project to %s", project_dir)
shutil.copytree(testapp_dir, project_dir)
logging.info("Changing directory to %s", project_dir)
os.chdir(project_dir)
# TODO(DDB): remove
# _run_setup_script(repo_dir, project_dir)
failures = []
if _DESKTOP in platforms:
logging.info("BEGIN %s, %s", testapp, _DESKTOP)
try:
_build_desktop(sdk_dir, cmake_flags)
except subprocess.SubprocessError as e:
failures.append(
Failure(testapp=testapp, platform=_DESKTOP, error_message=str(e)))
_rm_dir_safe(os.path.join(project_dir, "bin"))
logging.info("END %s, %s", testapp, _DESKTOP)
if _ANDROID in platforms:
logging.info("BEGIN %s, %s", testapp, _ANDROID)
try:
_validate_android_environment_variables()
_build_android(project_dir, sdk_dir)
except subprocess.SubprocessError as e:
failures.append(
Failure(testapp=testapp, platform=_ANDROID, error_message=str(e)))
_rm_dir_safe(os.path.join(project_dir, "build", "intermediates"))
_rm_dir_safe(os.path.join(project_dir, ".externalNativeBuild"))
logging.info("END %s, %s", testapp, _ANDROID)
if _IOS in platforms:
logging.info("BEGIN %s, %s", testapp, _IOS)
try:
_build_apple(
sdk_dir=sdk_dir,
xcframework_exist=xcframework_exist,
project_dir=project_dir,
repo_dir=repo_dir,
api_config=api_config,
target=api_config.ios_target,
scheme=api_config.ios_scheme,
apple_platfrom=_IOS,
apple_sdk=ios_sdk)
except subprocess.SubprocessError as e:
failures.append(
Failure(testapp=testapp, platform=_IOS, error_message=str(e)))
logging.info("END %s, %s", testapp, _IOS)
if _TVOS in platforms and api_config.tvos_target:
logging.info("BEGIN %s, %s", testapp, _TVOS)
try:
_build_apple(
sdk_dir=sdk_dir,
xcframework_exist=xcframework_exist,
project_dir=project_dir,
repo_dir=repo_dir,
api_config=api_config,
target=api_config.tvos_target,
scheme=api_config.tvos_scheme,
apple_platfrom=_TVOS,
apple_sdk=tvos_sdk)
except subprocess.SubprocessError as e:
failures.append(
Failure(testapp=testapp, platform=_TVOS, error_message=str(e)))
logging.info("END %s, %s", testapp, _TVOS)
return failures
def _collect_integration_tests(testapps, root_output_dir, output_dir, artifact_name):
testapps_artifact_dir = "testapps-" + artifact_name
android_testapp_extension = ".apk"
ios_testapp_extension = ".ipa"
ios_simualtor_testapp_extension = ".app"
desktop_testapp_name = "testapp"
if platform.system() == "Windows":
desktop_testapp_name += ".exe"
testapp_paths = []
testapp_google_services = {}
for file_dir, directories, file_names in os.walk(output_dir):
for directory in directories:
if directory.endswith(ios_simualtor_testapp_extension):
testapp_paths.append(os.path.join(file_dir, directory))
for file_name in file_names:
if ((file_name == desktop_testapp_name and "ios_build" not in file_dir)
or file_name.endswith(android_testapp_extension)
or file_name.endswith(ios_testapp_extension)):
testapp_paths.append(os.path.join(file_dir, file_name))
if (file_name == "google-services.json"):
testapp_google_services[file_dir.split(os.path.sep)[-2]] = os.path.join(file_dir, file_name)
artifact_path = os.path.join(root_output_dir, testapps_artifact_dir)
_rm_dir_safe(artifact_path)
for testapp in testapps:
os.makedirs(os.path.join(artifact_path, testapp))
for path in testapp_paths:
for testapp in testapps:
if testapp in path:
if os.path.isfile(path):
shutil.copy(path, os.path.join(artifact_path, testapp))
if path.endswith(desktop_testapp_name) and testapp_google_services.get(testapp):
shutil.copy(testapp_google_services[testapp], os.path.join(artifact_path, testapp))
else:
dir_util.copy_tree(path, os.path.join(artifact_path, testapp, os.path.basename(path)))
break
def _write_summary(testapp_dir, summary, file_name="summary.log"):
with open(os.path.join(testapp_dir, file_name), "a") as f:
timestamp = datetime.datetime.now().strftime("%Y_%m_%d-%H_%M_%S")
f.write("\n%s\n%s\n" % (timestamp, summary))
def _summarize_results(testapps, platforms, failures, root_output_dir, artifact_name):
"""Logs a readable summary of the results of the build."""
file_name = "build-results-" + artifact_name + ".log"
summary = []
summary.append("BUILD SUMMARY:")
summary.append("TRIED TO BUILD: " + ",".join(testapps))
summary.append("ON PLATFORMS: " + ",".join(platforms))
if not failures:
summary.append("ALL BUILDS SUCCEEDED")
else:
summary.append("SOME ERRORS OCCURRED:")
for i, failure in enumerate(failures, start=1):
summary.append("%d: %s" % (i, failure.describe()))
summary = "\n".join(summary)
logging.info(summary)
_write_summary(root_output_dir, summary, file_name=file_name)
summary_json = {}
summary_json["type"] = "build"
summary_json["testapps"] = testapps
summary_json["errors"] = {failure.testapp:failure.error_message for failure in failures}
with open(os.path.join(root_output_dir, file_name+".json"), "a") as f:
f.write(json.dumps(summary_json, indent=2))
def _build_desktop(sdk_dir, cmake_flags):
cmake_configure_cmd = ["cmake", ".", "-DCMAKE_BUILD_TYPE=Debug",
"-DFIREBASE_CPP_SDK_DIR=" + sdk_dir]
if utils.is_windows_os():
cmake_configure_cmd += ["-A",
"Win32" if FLAGS.arch == "x86" else FLAGS.arch]
elif utils.is_mac_os():
# Ensure that correct Mac architecture is built.
cmake_configure_cmd += ["-DCMAKE_OSX_ARCHITECTURES=%s" %
("arm64" if FLAGS.arch == "arm64" else "x86_64")]
_run(cmake_configure_cmd + cmake_flags)
_run(["cmake", "--build", ".", "--config", "Debug"] +
["-j", str(FLAGS.jobs)] if FLAGS.jobs > 0 else [])
def _get_desktop_compiler_flags(compiler, compiler_table):
"""Returns the command line flags for this compiler."""
if not compiler: # None is an acceptable default value
return []
try:
return compiler_table[compiler]
except KeyError:
valid_keys = ", ".join(compiler_table.keys())
raise ValueError(
"Given compiler: %s. Valid compilers: %s" % (compiler, valid_keys))
def _build_android(project_dir, sdk_dir):
"""Builds an Android binary (apk)."""
if platform.system() == "Windows":
gradlew = "gradlew.bat"
sdk_dir = sdk_dir.replace("\\", "/") # Gradle misinterprets backslashes.
else:
gradlew = "./gradlew"
logging.info("Patching gradle properties with path to SDK")
gradle_properties = os.path.join(project_dir, "gradle.properties")
with open(gradle_properties, "a+") as f:
f.write("systemProp.firebase_cpp_sdk.dir=" + sdk_dir + "\n")
f.write("http.keepAlive=false\n")
f.write("maven.wagon.http.pool=false\n")
f.write("maven.wagon.httpconnectionManager.ttlSeconds=120")
# This will log the versions of dependencies for debugging purposes.
_run([gradlew, "dependencies", "--configuration", "debugCompileClasspath",])
_run([gradlew, "assembleDebug", "--stacktrace"])
def _validate_android_environment_variables():
"""Checks environment variables that may be required for Android."""
# Ultimately we let the gradle build be the source of truth on what env vars
# are required, but try to repair holes and log warnings if we can't.
android_home = os.environ.get(_ANDROID_HOME)
if not os.environ.get(_JAVA_HOME):
logging.warning("%s not set", _JAVA_HOME)
if not os.environ.get(_ANDROID_SDK_HOME):
if android_home: # Use ANDROID_HOME as backup for ANDROID_SDK_HOME
os.environ[_ANDROID_SDK_HOME] = android_home
logging.info("%s not found, using %s", _ANDROID_SDK_HOME, _ANDROID_HOME)
else:
logging.warning("Missing: %s and %s", _ANDROID_SDK_HOME, _ANDROID_HOME)
# Different environments may have different NDK env vars specified. We look
# for these, in this order, and set the others to the first found.
# If none are set, we check the default location for the ndk.
ndk_path = None
ndk_vars = [_NDK_ROOT, _ANDROID_NDK_HOME]
for env_var in ndk_vars:
val = os.environ.get(env_var)
if val:
ndk_path = val
break
if not ndk_path:
if android_home:
default_ndk_path = os.path.join(android_home, "ndk-bundle")
if os.path.isdir(default_ndk_path):
ndk_path = default_ndk_path
if ndk_path:
logging.info("Found ndk: %s", ndk_path)
for env_var in ndk_vars:
if os.environ.get(env_var) != ndk_path:
logging.info("Setting %s to %s", env_var, ndk_path)
os.environ[env_var] = ndk_path
else:
logging.warning("No NDK env var set. Set one of %s", ", ".join(ndk_vars))
# build required ios xcframeworks based on makefiles
# the xcframeworks locates at repo_dir/ios_build
def _build_xcframework_from_repo(repo_dir, apple_platform, testapps, config):
"""Builds xcframework from SDK source."""
output_path = os.path.join(repo_dir, apple_platform + "_build")
_rm_dir_safe(output_path)
xcframework_builder = os.path.join(
repo_dir, "scripts", "gha", "build_ios_tvos.py")
# build only required targets to save time
target = set()
for testapp in testapps:
api_config = config.get_api(testapp)
if apple_platform == "ios" or (apple_platform == "tvos" and api_config.tvos_target):
for framework in api_config.frameworks:
# firebase_analytics.framework -> firebase_analytics
target.add(os.path.splitext(framework)[0])
# firebase is not a target in CMake, firebase_app is the target
# firebase_app will be built by other target as well
target.remove("firebase")
framework_builder_args = [
sys.executable, xcframework_builder,
"-b", output_path,
"-s", repo_dir,
"-o", apple_platform,
"-t"
]
framework_builder_args.extend(target)
_run(framework_builder_args)
def _build_apple(
sdk_dir, xcframework_exist, project_dir, repo_dir, api_config,
target, scheme, apple_platfrom, apple_sdk):
"""Builds an iOS application (.app, .ipa or both)."""
build_dir = apple_platfrom.lower() + "_build"
if not xcframework_exist:
sdk_dir = os.path.join(repo_dir, build_dir)
build_dir = os.path.join(project_dir, build_dir)
os.makedirs(build_dir)
logging.info("Copying XCFrameworks")
framework_src_dir = os.path.join(sdk_dir, "xcframeworks")
framework_paths = [] # Paths to the copied frameworks.
for framework in api_config.frameworks:
framework_src_path = os.path.join(framework_src_dir, framework)
framework_dest_path = os.path.join(project_dir, "Frameworks", framework)
dir_util.copy_tree(framework_src_path, framework_dest_path)
framework_paths.append(framework_dest_path)
_run(["pod", "install"])
entitlements_path = os.path.join(
project_dir, api_config.ios_target + ".entitlements")
xcode_tool_path = os.path.join(
repo_dir, "scripts", "gha", "integration_testing", "xcode_tool.rb")
xcode_patcher_args = [
"ruby", xcode_tool_path,
"--XCodeCPP.xcodeProjectDir", project_dir,
"--XCodeCPP.target", target,
"--XCodeCPP.frameworks", ",".join(framework_paths)
]
# Internal integration tests require the SDK root as an include path.
if repo_dir and api_config.internal_testapp_path:
xcode_patcher_args.extend(("--XCodeCPP.include", repo_dir))
if os.path.isfile(entitlements_path): # Not all testapps require entitlements
logging.info("Entitlements file detected.")
xcode_patcher_args.extend(("--XCodeCPP.entitlement", entitlements_path))
else:
logging.info("No entitlements found at %s.", entitlements_path)
_run(xcode_patcher_args)
xcode_path = os.path.join(project_dir, "integration_test.xcworkspace")
if _APPLE_SDK_SIMULATOR in apple_sdk:
_run(
xcodebuild.get_args_for_build(
path=xcode_path,
scheme=scheme,
output_dir=build_dir,
apple_platfrom=apple_platfrom,
apple_sdk=_APPLE_SDK_SIMULATOR,
configuration="Debug"))
if _APPLE_SDK_DEVICE in apple_sdk:
_run(
xcodebuild.get_args_for_build(
path=xcode_path,
scheme=scheme,
output_dir=build_dir,
apple_platfrom=apple_platfrom,
apple_sdk=_APPLE_SDK_DEVICE,
configuration="Debug"))
xcodebuild.generate_unsigned_ipa(
output_dir=build_dir, configuration="Debug")
# This should be executed before performing any builds.
def _run_setup_script(root_dir, testapp_dir):
"""Runs the setup_integration_tests.py script."""
# This script will download gtest to its own directory.
# The CMake projects were configured to download gtest, but this was
# found to be flaky and errors didn't propagate up the build system
# layers. The workaround is to download gtest with this script and copy it.
downloader_dir = os.path.join(root_dir, "testing", "test_framework")
_run([sys.executable, os.path.join(downloader_dir, "download_googletest.py")])
# Copies shared test framework files into the project, including gtest.
script_path = os.path.join(root_dir, "setup_integration_tests.py")
_run([sys.executable, script_path, testapp_dir])
def _run(args, timeout=_DEFAULT_RUN_TIMEOUT_SECONDS, capture_output=False, text=None, check=True):
"""Executes a command in a subprocess."""
logging.info("Running in subprocess: %s", " ".join(args))
return subprocess.run(
args=args,
timeout=timeout,
capture_output=capture_output,
text=text,
check=check)
def _handle_readonly_file(func, path, excinfo):
"""Function passed into shutil.rmtree to handle Access Denied error"""
os.chmod(path, stat.S_IWRITE)
func(path) # will re-throw if a different error occurrs
def _rm_dir_safe(directory_path):
"""Removes directory at given path. No error if dir doesn't exist."""
logging.info("Deleting %s...", directory_path)
try:
shutil.rmtree(directory_path, onerror=_handle_readonly_file)
except OSError as e:
# There are two known cases where this can happen:
# The directory doesn't exist (FileNotFoundError)
# A file in the directory is open in another process (PermissionError)
logging.warning("Failed to remove directory:\n%s", e.strerror)
def _fix_path(path):
"""Expands ~, normalizes slashes, and converts relative paths to absolute."""
return os.path.abspath(os.path.expanduser(path))
@attr.s(frozen=True, eq=False)
class Failure(object):
"""Holds context for the failure of a testapp to build/run."""
testapp = attr.ib()
platform = attr.ib()
error_message = attr.ib()
def describe(self):
return "%s, %s: %s" % (self.testapp, self.platform, self.error_message)
if __name__ == "__main__":
flags.mark_flag_as_required("testapps")
flags.mark_flag_as_required("platforms")
flags.mark_flag_as_required("packaged_sdk")
app.run(main)