-
Notifications
You must be signed in to change notification settings - Fork 5.6k
/
Copy pathintegrate_bazel.py
1488 lines (1245 loc) · 59.1 KB
/
integrate_bazel.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
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
import atexit
import errno
import getpass
import glob
import hashlib
import json
import os
import platform
import queue
import shlex
import shutil
import socket
import stat
import subprocess
import sys
import threading
import time
import urllib.request
from io import StringIO
from typing import Any, Dict, List, Set, Tuple
import distro
import git
import psutil
import requests
import SCons
from retry import retry
from retry.api import retry_call
from SCons.Script import ARGUMENTS
from buildscripts.install_bazel import install_bazel
# Disable retries locally
_LOCAL_MAX_RETRY_ATTEMPTS = 1
# Enable up to 3 attempts in
_CI_MAX_RETRY_ATTEMPTS = 3
_SUPPORTED_PLATFORM_MATRIX = [
"linux:arm64:gcc",
"linux:arm64:clang",
"linux:amd64:gcc",
"linux:amd64:clang",
"linux:ppc64le:gcc",
"linux:ppc64le:clang",
"linux:s390x:gcc",
"linux:s390x:clang",
"windows:amd64:msvc",
"macos:amd64:clang",
"macos:arm64:clang",
]
_SANITIZER_MAP = {
"address": "asan",
"fuzzer": "fsan",
"memory": "msan",
"leak": "lsan",
"thread": "tsan",
"undefined": "ubsan",
}
_DISTRO_PATTERN_MAP = {
"Ubuntu 18*": "ubuntu18",
"Ubuntu 20*": "ubuntu20",
"Ubuntu 22*": "ubuntu22",
"Ubuntu 24*": "ubuntu24",
"Amazon Linux 2": "amazon_linux_2",
"Amazon Linux 2023": "amazon_linux_2023",
"Debian GNU/Linux 10": "debian10",
"Debian GNU/Linux 12": "debian12",
"Red Hat Enterprise Linux 8*": "rhel8",
"Red Hat Enterprise Linux 9*": "rhel9",
"SLES 15*": "suse15",
}
_S3_HASH_MAPPING = {
"https://mdb-build-public.s3.amazonaws.com/bazel-binaries/bazel-7.2.1-ppc64le": "4ecc7f1396b8d921c6468b34cc8ed356c4f2dbe8a154c25d681a61ccb5dfc9cb",
"https://mdb-build-public.s3.amazonaws.com/bazel-binaries/bazel-7.2.1-s390x": "2f5f7fd747620d96e885766a4027347c75c0f455c68219211a00e72fc6413be9",
"https://mdb-build-public.s3.amazonaws.com/bazelisk-binaries/v1.19.0/bazelisk-darwin-amd64": "f2ba5f721a995b54bab68c6b76a340719888aa740310e634771086b6d1528ecd",
"https://mdb-build-public.s3.amazonaws.com/bazelisk-binaries/v1.19.0/bazelisk-darwin-arm64": "69fa21cd2ccffc2f0970c21aa3615484ba89e3553ecce1233a9d8ad9570d170e",
"https://mdb-build-public.s3.amazonaws.com/bazelisk-binaries/v1.19.0/bazelisk-linux-amd64": "d28b588ac0916abd6bf02defb5433f6eddf7cba35ffa808eabb65a44aab226f7",
"https://mdb-build-public.s3.amazonaws.com/bazelisk-binaries/v1.19.0/bazelisk-linux-arm64": "861a16ba9979613e70bd3d2f9d9ab5e3b59fe79471c5753acdc9c431ab6c9d94",
"https://mdb-build-public.s3.amazonaws.com/bazelisk-binaries/v1.19.0/bazelisk-windows-amd64.exe": "d04555245a99dfb628e33da24e2b9198beb8f46d7e7661c313eb045f6a59f5e4",
}
class Globals:
# key: scons target, value: {bazel target, bazel output}
scons2bazel_targets: Dict[str, Dict[str, str]] = dict()
# key: scons output, value: bazel outputs
scons_output_to_bazel_outputs: Dict[str, List[str]] = dict()
# targets bazel needs to build
bazel_targets_work_queue: queue.Queue[str] = queue.Queue()
# targets bazel has finished building
bazel_targets_done: Set[str] = set()
# lock for accessing the targets done list
bazel_target_done_CV: threading.Condition = threading.Condition()
# bazel command line with options, but not targets
bazel_base_build_command: List[str] = None
# environment variables to set when invoking bazel
bazel_env_variables: Dict[str, str] = {}
# Flag to signal that scons is ready to build, but needs to wait on bazel
waiting_on_bazel_flag: bool = False
# Flag to signal that scons is ready to build, but needs to wait on bazel
bazel_build_success: bool = False
bazel_build_exitcode: int = 1
# a IO object to hold the bazel output in place of stdout
bazel_thread_terminal_output = StringIO()
bazel_executable = None
max_retry_attempts: int = _LOCAL_MAX_RETRY_ATTEMPTS
@staticmethod
def bazel_output(scons_node):
return Globals.scons2bazel_targets[str(scons_node).replace("\\", "/")]["bazel_output"]
@staticmethod
def bazel_target(scons_node):
return Globals.scons2bazel_targets[str(scons_node).replace("\\", "/")]["bazel_target"]
@staticmethod
def bazel_link_file(scons_node):
bazel_target = Globals.scons2bazel_targets[str(scons_node).replace("\\", "/")][
"bazel_target"
]
linkfile = bazel_target.replace("//src/", "bazel-bin/src/") + "_links.list"
return "/".join(linkfile.rsplit(":", 1))
@staticmethod
def bazel_sources_file(scons_node):
bazel_target = Globals.scons2bazel_targets[str(scons_node).replace("\\", "/")][
"bazel_target"
]
sources_file = (
bazel_target.replace("//src/", "bazel-bin/src/") + "_sources_list.sources_list"
)
return "/".join(sources_file.rsplit(":", 1))
def bazel_debug(msg: str):
pass
def bazel_target_emitter(
target: List[SCons.Node.Node], source: List[SCons.Node.Node], env: SCons.Environment.Environment
) -> Tuple[List[SCons.Node.Node], List[SCons.Node.Node]]:
"""This emitter will map any scons outputs to bazel outputs so copy can be done later."""
for t in target:
# bazel will cache the results itself, don't recache
env.NoCache(t)
return (target, source)
def bazel_builder_action(
env: SCons.Environment.Environment, target: List[SCons.Node.Node], source: List[SCons.Node.Node]
):
if env.GetOption("separate-debug") == "on":
shlib_suffix = env.subst("$SHLIBSUFFIX")
sep_dbg = env.subst("$SEPDBG_SUFFIX")
if sep_dbg and str(target[0]).endswith(shlib_suffix):
target.append(env.File(str(target[0]) + sep_dbg))
# now copy all the targets out to the scons tree, note that target is a
# list of nodes so we need to stringify it for copyfile
for t in target:
dSYM_found = False
if ".dSYM/" in str(t):
# ignore dSYM plist file, as we skipped it prior
if str(t).endswith(".plist"):
continue
dSYM_found = True
if dSYM_found:
# Here we handle the difference between scons and bazel for dSYM dirs. SCons uses list
# actions to perform operations on the same target during some action. Bazel does not
# have an exact corresponding feature. Each action in bazel should have unique inputs and
# outputs. The file and targets wont line up exactly between scons and our mongo_cc_library,
# custom rule, specifically the way dsymutil generates the dwarf file inside the dSYM dir. So
# we remap the special filename suffixes we use for our bazel intermediate cc_library rules.
#
# So we will do the renaming of dwarf file to what scons expects here, before we copy to scons tree
substring_end = str(t).find(".dSYM/") + 5
t = str(t)[:substring_end]
# This is declared as an output folder, so bazel appends (TreeArtifact) to it
s = Globals.bazel_output(t + " (TreeArtifact)")
s = str(s).removesuffix(" (TreeArtifact)")
dwarf_info_base = os.path.splitext(os.path.splitext(os.path.basename(t))[0])[0]
dwarf_sym_with_debug = os.path.join(
s, f"Contents/Resources/DWARF/{dwarf_info_base}_shared_with_debug.dylib"
)
# this handles shared libs or program binaries
if os.path.exists(dwarf_sym_with_debug):
dwarf_sym = os.path.join(s, f"Contents/Resources/DWARF/{dwarf_info_base}.dylib")
else:
dwarf_sym_with_debug = os.path.join(
s, f"Contents/Resources/DWARF/{dwarf_info_base}_with_debug"
)
dwarf_sym = os.path.join(s, f"Contents/Resources/DWARF/{dwarf_info_base}")
# copy the whole dSYM in one operation. Clean any existing files that might be in the way.
print(f"Moving .dSYM from {s} over to {t}.")
shutil.rmtree(str(t), ignore_errors=True)
shutil.copytree(s, str(t))
# we want to change the permissions back to normal permissions on the folders copied rather than read only
os.chmod(t, 0o755)
for root, dirs, files in os.walk(t):
for name in files:
os.chmod(os.path.join(root, name), 0o755)
for name in dirs:
os.chmod(os.path.join(root, name), 0o755)
# shouldn't write our own files to the bazel directory, renaming file for scons
shutil.copy(dwarf_sym_with_debug.replace(s, t), dwarf_sym.replace(s, t))
else:
s = Globals.bazel_output(t)
try:
# Check if the current directory and .cache files are on the same mount
# because hardlinking doesn't work between drives and when it fails
# it leaves behind a symlink that is hard to clean up
# We don't hardlink on windows because SCons will run link commands against
# the files in the bazel directory, and if its running the link command
# while SCons cleans up files in the output directory you get file permission errors
if (
platform.system() != "Windows"
and os.stat(".").st_dev == os.stat(s, follow_symlinks=True).st_dev
):
if os.path.exists(str(t)):
os.remove(str(t))
os.link(s, str(t))
os.chmod(str(t), os.stat(str(t)).st_mode | stat.S_IWUSR)
else:
print(
f"Copying {s} to {t} instead of hardlinking because files are on different mounts or we are on Windows."
)
shutil.copy(s, str(t))
os.chmod(str(t), os.stat(str(t)).st_mode | stat.S_IWUSR)
# Fall back on the original behavior of copying, likely if we hit here this
# will still fail due to hardlinking leaving some symlinks around
except Exception as e:
print(e)
print(f"Failed to hardlink {s} to {t}, trying to copying file instead.")
shutil.copy(s, str(t))
os.chmod(str(t), os.stat(str(t)).st_mode | stat.S_IWUSR)
BazelCopyOutputsAction = SCons.Action.FunctionAction(
bazel_builder_action,
{"cmdstr": "Hardlinking $TARGETS from bazel build directory.", "varlist": ["BAZEL_FLAGS_STR"]},
)
total_query_time = 0
total_queries = 0
def bazel_query_func(
env: SCons.Environment.Environment, query_command_args: List[str], query_name: str = "query"
):
full_command = [Globals.bazel_executable] + query_command_args
global total_query_time, total_queries
start_time = time.time()
# these args prune the graph we need to search through a bit since we only care about our
# specific library target dependencies
full_command += ["--implicit_deps=False", "--tool_deps=False", "--include_aspects=False"]
# prevent remote connection and invocations since we just want to query the graph
full_command += [
"--remote_executor=",
"--remote_cache=",
"--bes_backend=",
"--bes_results_url=",
]
bazel_debug(f"Running query: {' '.join(full_command)}")
results = subprocess.run(
full_command,
capture_output=True,
text=True,
cwd=env.Dir("#").abspath,
env={**os.environ.copy(), **Globals.bazel_env_variables},
)
delta = time.time() - start_time
bazel_debug(f"Spent {delta} seconds running {query_name}")
total_query_time += delta
total_queries += 1
# Manually throw the error instead of using subprocess.run(... check=True) to print out stdout and stderr.
if results.returncode != 0:
print(results.stdout)
print(results.stderr)
raise subprocess.CalledProcessError(
results.returncode, full_command, results.stdout, results.stderr
)
return results
# the ninja tool has some API that doesn't support using SCons env methods
# instead of adding more API to the ninja tool which has a short life left
# we just add the unused arg _dup_env
def ninja_bazel_builder(
env: SCons.Environment.Environment,
_dup_env: SCons.Environment.Environment,
node: SCons.Node.Node,
) -> Dict[str, Any]:
"""
Translator for ninja which turns the scons bazel_builder_action
into a build node that ninja can digest.
"""
outs = env.NinjaGetOutputs(node)
ins = [Globals.bazel_output(out) for out in outs]
# this represents the values the ninja_syntax.py will use to generate to real
# ninja syntax defined in the ninja manaul: https://ninja-build.org/manual.html#ref_ninja_file
return {
"outputs": outs,
"inputs": ins,
"rule": "BAZEL_COPY_RULE",
"variables": {
"cmd": " && ".join(
[
f"$COPY {input_node.replace('/',os.sep)} {output_node}"
for input_node, output_node in zip(ins, outs)
]
+ [
# Touch output files to make sure that the modified time of inputs is always older than the modified time of outputs.
f"copy /b {output_node} +,, {output_node}"
if env["PLATFORM"] == "win32"
else f"touch {output_node}"
for output_node in outs
]
)
},
}
def write_bazel_build_output(line: str) -> None:
if Globals.waiting_on_bazel_flag:
if Globals.bazel_thread_terminal_output is not None:
Globals.bazel_thread_terminal_output.seek(0)
sys.stdout.write(Globals.bazel_thread_terminal_output.read())
Globals.bazel_thread_terminal_output = None
sys.stdout.write(line)
else:
Globals.bazel_thread_terminal_output.write(line)
def perform_tty_bazel_build(bazel_cmd: str) -> None:
# Importing pty will throw on certain platforms, the calling code must catch this exception
# and fallback to perform_non_tty_bazel_build.
import pty
parent_fd, child_fd = pty.openpty() # provide tty
bazel_proc = subprocess.Popen(
bazel_cmd,
stdin=child_fd,
stdout=child_fd,
stderr=subprocess.STDOUT,
env={**os.environ.copy(), **Globals.bazel_env_variables},
)
os.close(child_fd)
# Timeout when stuck scheduling without making progress for more than 10 minutes
# Ex string:
# [21,537 / 21,603] [Sched] Compiling src/mongo/db/s/migration_chunk_cloner_source.cpp; 1424s
last_sched_target_progress = ""
sched_time_start = 0
sched_timeout_sec = 60 * 10
try:
# This loop will terminate with an EOF or EOI when the process ends.
while True:
try:
data = os.read(parent_fd, 512)
except OSError as e:
if e.errno != errno.EIO:
raise
break # EIO means EOF on some systems
else:
if not data: # EOF
break
line = data.decode()
write_bazel_build_output(line)
if "[Sched]" in line:
target_progress = line.split("[Sched]")[0].strip()
if len(target_progress) > 0:
if last_sched_target_progress == target_progress:
if time.time() - sched_time_start > sched_timeout_sec:
write_bazel_build_output("Stuck scheduling for too long, terminating")
bazel_proc.kill()
bazel_proc.wait()
raise subprocess.CalledProcessError(-1, bazel_cmd, "", "")
else:
sched_time_start = time.time()
last_sched_target_progress = target_progress
finally:
os.close(parent_fd)
if bazel_proc.poll() is None:
bazel_proc.kill()
bazel_proc.wait()
Globals.bazel_build_exitcode = bazel_proc.returncode
if bazel_proc.returncode != 0:
raise subprocess.CalledProcessError(bazel_proc.returncode, bazel_cmd, "", "")
def perform_non_tty_bazel_build(bazel_cmd: str) -> None:
bazel_proc = subprocess.Popen(
bazel_cmd,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
env={**os.environ.copy(), **Globals.bazel_env_variables},
text=True,
)
# This loop will terminate when the process ends.
while True:
line = bazel_proc.stdout.readline()
if not line:
break
write_bazel_build_output(line)
stdout, stderr = bazel_proc.communicate()
Globals.bazel_build_exitcode = bazel_proc.returncode
if bazel_proc.returncode != 0:
raise subprocess.CalledProcessError(bazel_proc.returncode, bazel_cmd, stdout, stderr)
def run_bazel_command(env, bazel_cmd, tries_so_far=0):
try:
tty_import_fail = False
try:
retry_call(
perform_tty_bazel_build,
[bazel_cmd],
tries=Globals.max_retry_attempts,
exceptions=(subprocess.CalledProcessError,),
)
except ImportError:
# Run the actual build outside of the except clause to avoid confusion in the stack trace,
# otherwise, build failures on platforms that don't support tty will be displayed as import errors.
tty_import_fail = True
pass
if tty_import_fail:
retry_call(
perform_non_tty_bazel_build,
[bazel_cmd],
tries=Globals.max_retry_attempts,
exceptions=(subprocess.CalledProcessError,),
)
except subprocess.CalledProcessError as ex:
if platform.system() == "Windows" and tries_so_far == 0:
print(
"Build failed, retrying with --jobs=4 in case linking failed due to hitting concurrency limits..."
)
run_bazel_command(
env, bazel_cmd + ["--jobs", "4", "--link_timeout_8min=False"], tries_so_far=1
)
return
print("ERROR: Bazel build failed:")
if Globals.bazel_thread_terminal_output is not None:
Globals.bazel_thread_terminal_output.seek(0)
ex.output += Globals.bazel_thread_terminal_output.read()
Globals.bazel_thread_terminal_output = None
print(ex.output)
raise ex
Globals.bazel_build_success = True
def bazel_build_thread_func(env, log_dir: str, verbose: bool, ninja_generate: bool) -> None:
"""This thread runs the bazel build up front."""
if verbose:
extra_args = []
else:
extra_args = ["--output_filter=DONT_MATCH_ANYTHING"]
if ninja_generate:
for file in glob.glob("bazel-out/**/*.gen_source_list", recursive=True):
os.chmod(file, stat.S_IWRITE)
os.remove(file)
extra_args += ["--build_tag_filters=scons_link_lists"]
bazel_cmd = Globals.bazel_base_build_command + extra_args + ["//src/..."]
elif SCons.Script.BUILD_TARGETS == ["compiledb"]:
extra_args += ["--build_tag_filters=scons_link_lists,compiledb,gen_source"]
bazel_cmd = Globals.bazel_base_build_command + extra_args + ["//:compiledb", "//src/..."]
elif SCons.Script.BUILD_TARGETS == ["compiledb", "+mongo-tidy-tests"]:
extra_args += [
"--build_tag_filters=scons_link_lists,compiledb,gen_source,mongo-tidy-tests,mongo-tidy-checks"
]
bazel_cmd = Globals.bazel_base_build_command + extra_args + ["//:compiledb", "//src/..."]
else:
build_tags = env.GetOption("bazel-build-tag")
if not build_tags:
build_tags += ["all"]
if "all" not in build_tags:
build_tags += ["scons_link_lists", "gen_source"]
extra_args += [f"--build_tag_filters={','.join(build_tags)}"]
bazel_cmd = Globals.bazel_base_build_command + extra_args + ["//src/..."]
if ninja_generate:
print("Generating bazel link deps...")
else:
print(f"Bazel build command:\n{' '.join(bazel_cmd)}")
if env.GetOption("coverity-build"):
print(f"BAZEL_COMMAND: {' '.join(bazel_cmd)}")
return
print("Starting bazel build thread...")
run_bazel_command(env, bazel_cmd)
def create_bazel_builder(builder: SCons.Builder.Builder) -> SCons.Builder.Builder:
return SCons.Builder.Builder(
action=BazelCopyOutputsAction,
prefix=builder.prefix,
suffix=builder.suffix,
src_suffix=builder.src_suffix,
source_scanner=builder.source_scanner,
target_scanner=builder.target_scanner,
emitter=SCons.Builder.ListEmitter([builder.emitter, bazel_target_emitter]),
)
# TODO delete this builder when we have testlist support in bazel
def create_program_builder(env: SCons.Environment.Environment) -> None:
env["BUILDERS"]["BazelProgram"] = create_bazel_builder(env["BUILDERS"]["Program"])
def get_default_cert_dir():
if platform.system() == "Windows":
return f"C:/cygwin/home/{getpass.getuser()}/.engflow"
elif platform.system() == "Linux":
return f"/home/{getpass.getuser()}/.engflow"
elif platform.system() == "Darwin":
return f"{os.path.expanduser('~')}/.engflow"
def validate_remote_execution_certs(env: SCons.Environment.Environment) -> bool:
running_in_evergreen = os.environ.get("CI")
if running_in_evergreen and not os.path.exists("./engflow.cert"):
print(
"ERROR: ./engflow.cert not found, which is required to build in evergreen without BAZEL_FLAGS=--config=local set. Please reach out to #ask-devprod-build for help."
)
return False
if os.name == "nt" and not os.path.exists(f"{os.path.expanduser('~')}/.bazelrc"):
with open(f"{os.path.expanduser('~')}/.bazelrc", "a") as bazelrc:
bazelrc.write(
f"build --tls_client_certificate={get_default_cert_dir()}/creds/engflow.crt\n"
)
bazelrc.write(f"build --tls_client_key={get_default_cert_dir()}/creds/engflow.key\n")
if not running_in_evergreen and not os.path.exists(
f"{get_default_cert_dir()}/creds/engflow.crt"
):
# Temporary logic to copy over the credentials for users that ran the installation steps using the old directory (/engflow/).
if os.path.exists("/engflow/creds/engflow.crt") and os.path.exists(
"/engflow/creds/engflow.key"
):
print(
"Moving EngFlow credentials from the legacy directory (/engflow/) to the new directory (~/.engflow/)."
)
try:
os.makedirs(f"{get_default_cert_dir()}/creds/", exist_ok=True)
shutil.move(
"/engflow/creds/engflow.crt",
f"{get_default_cert_dir()}/creds/engflow.crt",
)
shutil.move(
"/engflow/creds/engflow.key",
f"{get_default_cert_dir()}/creds/engflow.key",
)
with open(f"{get_default_cert_dir()}/.bazelrc", "a") as bazelrc:
bazelrc.write(
f"build --tls_client_certificate={get_default_cert_dir()}/creds/engflow.crt\n"
)
bazelrc.write(
f"build --tls_client_key={get_default_cert_dir()}/creds/engflow.key\n"
)
except OSError as exc:
print(exc)
print(
"Failed to update cert location, please move them manually. Otherwise you can pass 'BAZEL_FLAGS=\"--config=local\"' on the SCons command line."
)
return True
# Pull the external hostname of the system from aws
try:
response = requests.get(
"http://instance-data.ec2.internal/latest/meta-data/public-hostname"
)
status_code = response.status_code
except Exception as _:
status_code = 500
if status_code == 200:
public_hostname = response.text
else:
public_hostname = "localhost"
print(
f"""\nERROR: {get_default_cert_dir()}/creds/engflow.crt not found. Please reach out to #ask-devprod-build if you need help with the steps below.
(If the below steps are not working or you are an external person to MongoDB, remote execution can be disabled by passing BAZEL_FLAGS=--config=local at the end of your scons.py invocation)
Please complete the following steps to generate a certificate:
- (If not in the Engineering org) Request access to the MANA group https://mana.corp.mongodbgov.com/resources/659ec4b9bccf3819e5608712
- Go to https://sodalite.cluster.engflow.com/gettingstarted (Uses mongodbcorp.okta.com auth URL)
- Login with OKTA, then click the \"GENERATE AND DOWNLOAD MTLS CERTIFICATE\" button
- (If logging in with OKTA doesn't work) Login with Google using your MongoDB email, then click the "GENERATE AND DOWNLOAD MTLS CERTIFICATE" button
- On your local system (usually your MacBook), open a terminal and run:
ZIP_FILE=~/Downloads/engflow-mTLS.zip
curl https://raw.githubusercontent.com/mongodb/mongo/master/buildscripts/setup_engflow_creds.sh -o setup_engflow_creds.sh
chmod +x ./setup_engflow_creds.sh
./setup_engflow_creds.sh {getpass.getuser()} {public_hostname} $ZIP_FILE {"local" if public_hostname == "localhost" else ""}\n"""
)
return False
if not running_in_evergreen and (
not os.access(f"{get_default_cert_dir()}/creds/engflow.crt", os.R_OK)
or not os.access(f"{get_default_cert_dir()}/creds/engflow.key", os.R_OK)
):
print(
f"Invalid permissions set on {get_default_cert_dir()}/creds/engflow.crt or {get_default_cert_dir()}/creds/engflow.key"
)
print("Please run the following command to fix the permissions:\n")
print(
f"sudo chown {getpass.getuser()}:{getpass.getuser()} {get_default_cert_dir()}/creds/engflow.crt {get_default_cert_dir()}/creds/engflow.key"
)
print(
f"sudo chmod 600 {get_default_cert_dir()}/creds/engflow.crt {get_default_cert_dir()}/creds/engflow.key"
)
return False
return True
def generate_bazel_info_for_ninja(env: SCons.Environment.Environment) -> None:
# create a json file which contains all the relevant info from this generation
# that bazel will need to construct the correct command line for any given targets
ninja_bazel_build_json = {
"bazel_cmd": Globals.bazel_base_build_command,
"compiledb_cmd": [Globals.bazel_executable, "run"]
+ env["BAZEL_FLAGS_STR"]
+ ["//:compiledb", "--"]
+ env["BAZEL_FLAGS_STR"],
"defaults": [str(t) for t in SCons.Script.DEFAULT_TARGETS],
"targets": Globals.scons2bazel_targets,
"CC": env.get("CC", ""),
"CXX": env.get("CXX", ""),
"USE_NATIVE_TOOLCHAIN": os.environ.get("USE_NATIVE_TOOLCHAIN"),
}
with open(f".{env.subst('$NINJA_PREFIX')}.bazel_info_for_ninja.txt", "w") as f:
json.dump(ninja_bazel_build_json, f)
# we also store the outputs in the env (the passed env is intended to be
# the same main env ninja tool is constructed with) so that ninja can
# use these to contruct a build node for running bazel where bazel list the
# correct bazel outputs to be copied to the scons tree. We also handle
# calculating the inputs. This will be the all the inputs of the outs,
# but and input can not also be an output. If a node is found in both
# inputs and outputs, remove it from the inputs, as it will be taken care
# internally by bazel build.
ninja_bazel_outs = []
ninja_bazel_ins = []
for scons_t, bazel_t in Globals.scons2bazel_targets.items():
ninja_bazel_outs += [bazel_t["bazel_output"]]
ninja_bazel_ins += env.NinjaGetInputs(env.File(scons_t))
if platform.system() == "Linux" and not os.environ.get("USE_NATIVE_TOOLCHAIN"):
ninja_bazel_outs += [env.get("CC"), env.get("CXX")]
# This is to be used directly by ninja later during generation of the ninja file
env["NINJA_BAZEL_OUTPUTS"] = ninja_bazel_outs
env["NINJA_BAZEL_INPUTS"] = ninja_bazel_ins
@retry(tries=5, delay=3)
def download_path_with_retry(*args, **kwargs):
urllib.request.urlretrieve(*args, **kwargs)
install_query_cache = {}
def bazel_deps_check_query_cache(env, bazel_target):
return install_query_cache.get(bazel_target, None)
def bazel_deps_add_query_cache(env, bazel_target, results):
install_query_cache[bazel_target] = results
link_query_cache = {}
def bazel_deps_check_link_query_cache(env, bazel_target):
return link_query_cache.get(bazel_target, None)
def bazel_deps_add_link_query_cache(env, bazel_target, results):
link_query_cache[bazel_target] = results
def sha256_file(filename: str) -> str:
sha256_hash = hashlib.sha256()
with open(filename, "rb") as f:
for block in iter(lambda: f.read(4096), b""):
sha256_hash.update(block)
return sha256_hash.hexdigest()
def verify_s3_hash(s3_path: str, local_path: str) -> None:
if s3_path not in _S3_HASH_MAPPING:
raise Exception(
"S3 path not found in hash mapping, unable to verify downloaded for s3 path: s3_path"
)
hash = sha256_file(local_path)
if hash != _S3_HASH_MAPPING[s3_path]:
raise Exception(
f"Hash mismatch for {s3_path}, expected {_S3_HASH_MAPPING[s3_path]} but got {hash}"
)
def find_distro_match(distro_str: str) -> str:
for distro_pattern, simplified_name in _DISTRO_PATTERN_MAP.items():
if "*" in distro_pattern:
prefix_suffix = distro_pattern.split("*")
if distro_str.startswith(prefix_suffix[0]) and distro_str.endswith(prefix_suffix[1]):
return simplified_name
elif distro_str == distro_pattern:
return simplified_name
return None
time_auto_installing = 0
count_of_auto_installing = 0
def timed_auto_install_bazel(env, libdep, shlib_suffix):
global time_auto_installing, count_of_auto_installing
start_time = time.time()
auto_install_bazel(env, libdep, shlib_suffix)
time_auto_installing += time.time() - start_time
count_of_auto_installing += 1
def auto_install_single_target(env, libdep, suffix, bazel_node):
auto_install_mapping = env["AIB_SUFFIX_MAP"].get(suffix)
env.AutoInstall(
target=auto_install_mapping.directory,
source=[bazel_node],
AIB_COMPONENT=env.get("AIB_COMPONENT", "AIB_DEFAULT_COMPONENT"),
AIB_ROLE=auto_install_mapping.default_role,
AIB_COMPONENTS_EXTRA=env.get("AIB_COMPONENTS_EXTRA", []),
)
auto_installed_libdep = env.GetAutoInstalledFiles(libdep)
auto_installed_bazel_node = env.GetAutoInstalledFiles(bazel_node)
if auto_installed_libdep[0] != auto_installed_bazel_node[0]:
env.Depends(auto_installed_libdep[0], auto_installed_bazel_node[0])
return env.GetAutoInstalledFiles(bazel_node)
def auto_install_bazel(env, libdep, shlib_suffix):
scons_target = str(libdep).replace(
f"{env.Dir('#').abspath}/{env['BAZEL_OUT_DIR']}/src", env.Dir("$BUILD_DIR").path
)
bazel_target = env["SCONS2BAZEL_TARGETS"].bazel_target(scons_target)
bazel_libdep = env.File(f"#/{env['SCONS2BAZEL_TARGETS'].bazel_output(scons_target)}")
query_results = env.CheckBazelDepsCache(bazel_target)
if query_results is None:
linkfile = env["SCONS2BAZEL_TARGETS"].bazel_link_file(scons_target)
with open(os.path.join(env.Dir("#").abspath, linkfile)) as f:
query_results = f.read()
filtered_results = ""
for lib in query_results.splitlines():
bazel_out_path = lib.replace(f"{env['BAZEL_OUT_DIR']}/src", "bazel-bin/src")
if os.path.exists(env.File("#/" + bazel_out_path + ".exclude_lib").abspath):
continue
filtered_results += lib + "\n"
query_results = filtered_results
env.AddBazelDepsCache(bazel_target, query_results)
for line in query_results.splitlines():
# We are only interested in installing shared libs and their debug files
if not line.endswith(shlib_suffix):
continue
bazel_node = env.File(f"#/{line}")
bazel_node_debug = env.File(f"#/{line}$SEPDBG_SUFFIX")
setattr(bazel_node_debug.attributes, "debug_file_for", bazel_node)
setattr(bazel_node.attributes, "separate_debug_files", [bazel_node_debug])
auto_install_single_target(env, bazel_libdep, shlib_suffix, bazel_node)
if env.GetAutoInstalledFiles(bazel_libdep):
auto_install_single_target(
env,
getattr(bazel_libdep.attributes, "separate_debug_files")[0],
env.subst("$SEPDBG_SUFFIX"),
bazel_node_debug,
)
return env.GetAutoInstalledFiles(libdep)
def auto_archive_bazel(env, node, already_archived, search_stack):
bazel_child = getattr(node.attributes, "AIB_INSTALL_FROM", node)
if not str(bazel_child).startswith("bazel-out"):
try:
bazel_child = env["SCONS2BAZEL_TARGETS"].bazel_output(bazel_child.path)
except KeyError:
return
if str(bazel_child) not in already_archived:
already_archived.add(str(bazel_child))
scons_target = str(bazel_child).replace(
f"{env['BAZEL_OUT_DIR']}/src", env.Dir("$BUILD_DIR").path
)
linkfile = env["SCONS2BAZEL_TARGETS"].bazel_link_file(scons_target)
with open(os.path.join(env.Dir("#").abspath, linkfile)) as f:
query_results = f.read()
filtered_results = ""
for lib in query_results.splitlines():
bazel_out_path = lib.replace("\\", "/").replace(
f"{env['BAZEL_OUT_DIR']}/src", "bazel-bin/src"
)
if os.path.exists(
env.File("#/" + bazel_out_path + ".exclude_lib").abspath.replace("\\", "/")
):
continue
filtered_results += lib + "\n"
query_results = filtered_results
for lib in query_results.splitlines():
if str(bazel_child).endswith(env.subst("$SEPDBG_SUFFIX")):
debug_file = getattr(env.File("#/" + lib).attributes, "separate_debug_files")[0]
bazel_install_file = env.GetAutoInstalledFiles(debug_file)[0]
else:
bazel_install_file = env.GetAutoInstalledFiles(env.File("#/" + lib))[0]
if bazel_install_file:
search_stack.append(bazel_install_file)
def load_bazel_builders(env):
# === Builders ===
create_program_builder(env)
if env.GetOption("ninja") != "disabled":
env.NinjaRule(
"BAZEL_COPY_RULE", "$env$cmd", description="Copy from Bazel", pool="local_pool"
)
total_libdeps_linking_time = 0
count_of_libdeps_links = 0
def add_libdeps_time(env, delate_time):
global total_libdeps_linking_time, count_of_libdeps_links
total_libdeps_linking_time += delate_time
count_of_libdeps_links += 1
def prefetch_toolchain(env):
setup_bazel_env_vars()
setup_max_retry_attempts()
bazel_bin_dir = (
env.GetOption("evergreen-tmp-dir")
if env.GetOption("evergreen-tmp-dir")
else os.path.expanduser("~/.local/bin")
)
if not os.path.exists(bazel_bin_dir):
os.makedirs(bazel_bin_dir)
Globals.bazel_executable = install_bazel(bazel_bin_dir)
if platform.system() == "Linux" and not ARGUMENTS.get("CC") and not ARGUMENTS.get("CXX"):
exec_root = f'bazel-{os.path.basename(env.Dir("#").abspath)}'
if exec_root and not os.path.exists(f"{exec_root}/external/mongo_toolchain"):
print("Prefetch the mongo toolchain...")
try:
retry_call(
subprocess.run,
[[Globals.bazel_executable, "build", "@mongo_toolchain", "--config=local"]],
fkwargs={
"env": {**os.environ.copy(), **Globals.bazel_env_variables},
"check": True,
},
tries=Globals.max_retry_attempts,
exceptions=(subprocess.CalledProcessError,),
)
except subprocess.CalledProcessError as ex:
print("ERROR: Bazel fetch failed!")
print(ex)
print("Please ask about this in #ask-devprod-build slack channel.")
sys.exit(1)
return exec_root
# Required boilerplate function
def exists(env: SCons.Environment.Environment) -> bool:
# === Bazelisk ===
write_workstation_bazelrc()
env.AddMethod(prefetch_toolchain, "PrefetchToolchain")
env.AddMethod(load_bazel_builders, "LoadBazelBuilders")
return True
def handle_bazel_program_exception(env, target, outputs):
prog_suf = env.subst("$PROGSUFFIX")
dbg_suffix = ".pdb" if sys.platform == "win32" else env.subst("$SEPDBG_SUFFIX")
bazel_program = False
# on windows the pdb for dlls contains no double extensions
# so we need to check all the outputs up front to know
for bazel_output_file in outputs:
if bazel_output_file.endswith(".dll"):
return False
if os.path.splitext(outputs[0])[1] in [prog_suf, dbg_suffix]:
for bazel_output_file in outputs:
first_ext = os.path.splitext(bazel_output_file)[1]
if dbg_suffix and first_ext == dbg_suffix:
second_ext = os.path.splitext(os.path.splitext(bazel_output_file)[0])[1]
else:
second_ext = None
if (
(second_ext is not None and second_ext + first_ext == prog_suf + dbg_suffix)
or (second_ext is None and first_ext == prog_suf)
or first_ext == ".exe"
or first_ext == ".pdb"
):
bazel_program = True
scons_node_str = bazel_output_file.replace(
f"{env['BAZEL_OUT_DIR']}/src", env.Dir("$BUILD_DIR").path.replace("\\", "/")
)
Globals.scons2bazel_targets[scons_node_str.replace("\\", "/")] = {
"bazel_target": target,
"bazel_output": bazel_output_file.replace("\\", "/"),
}
return bazel_program
def write_workstation_bazelrc():
if os.environ.get("CI") is None:
workstation_file = ".bazelrc.workstation"
existing_hash = ""
if os.path.exists(workstation_file):
with open(workstation_file) as f:
existing_hash = hashlib.md5(f.read().encode()).hexdigest()
try:
repo = git.Repo()
except Exception:
print(
"Unable to setup git repo, skipping workstation file generation. This will result in incomplete telemetry data being uploaded."
)
return
try:
status = "clean" if repo.head.commit.diff(None) is None else "modified"
except Exception:
status = "Unknown"