forked from facebook/buck
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathbuck.py
executable file
·297 lines (258 loc) · 11 KB
/
buck.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
#!/usr/bin/env python
# Copyright (c) Facebook, Inc. and its affiliates.
#
# 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.
from __future__ import print_function
import errno
import logging
import os
import re
import signal
import subprocess
import sys
import threading
import time
import uuid
import zipfile
from multiprocessing import Queue
from subprocess import check_output
from programs.buck_logging import setup_logging
from programs.buck_project import BuckProject, NoBuckConfigFoundException
from programs.buck_tool import (
BuckDaemonErrorException,
BuckStatusReporter,
ExecuteTarget,
ExitCode,
ExitCodeCallable,
install_signal_handlers,
)
from programs.java_lookup import get_java_path
from programs.java_version import get_java_major_version
from programs.subprocutils import propagate_failure
from programs.tracing import Tracing
if sys.version_info < (2, 7):
import platform
print(
(
"Buck requires at least version 2.7 of Python, but you are using {}."
"\nPlease follow https://buck.build/setup/getting_started.html "
+ "to properly setup your development environment."
).format(platform.version())
)
sys.exit(ExitCode.FATAL_BOOTSTRAP)
THIS_DIR = os.path.dirname(os.path.realpath(__file__))
# Kill all buck processes
def killall_buck(reporter):
# Linux or macOS
if os.name != "posix" and os.name != "nt":
message = "killall is not implemented on: " + os.name
logging.error(message)
reporter.status_message = message
return ExitCode.COMMANDLINE_ERROR
for line in os.popen("jps -l"):
split = line.split()
if len(split) == 1:
# Java processes which are launched not as `java Main`
# (e. g. `idea`) are shown with only PID without
# main class name.
continue
if len(split) != 2:
raise Exception("cannot parse a line in jps -l outout: " + repr(line))
pid = int(split[0])
name = split[1]
if name != "com.facebook.buck.cli.bootstrapper.ClassLoaderBootstrapper":
continue
os.kill(pid, signal.SIGTERM)
# TODO(buck_team) clean .buckd directories
return ExitCode.SUCCESS
def _get_java_version(java_path):
"""
Returns a Java version string (e.g. "7", "8").
Information is provided by java tool and parsing is based on
http://www.oracle.com/technetwork/java/javase/versioning-naming-139433.html
"""
java_version = check_output(
[java_path, "-version"], stderr=subprocess.STDOUT
).decode("utf-8")
# extract java version from a string like 'java version "1.8.0_144"' or
# 'openjdk version "11.0.1" 2018-10-16'
match = re.search('(java|openjdk) version "(?P<version>.+)"', java_version)
if not match:
return None
return get_java_major_version(match.group("version"))
def _try_to_verify_java_version(
java_version_status_queue, java_path, required_java_version
):
"""
Best effort check to make sure users have required Java version installed.
"""
warning = None
try:
java_version = _get_java_version(java_path)
if java_version and java_version != required_java_version:
warning = "You're using Java {}, but Buck requires Java {}.".format(
java_version, required_java_version
)
# warning += (
# " Please update JAVA_HOME if it's pointing at the wrong version of Java."
# + "\nPlease follow https://buck.build/setup/getting_started.html"
# + " to properly setup your local environment and avoid build issues."
# )
except:
# checking Java version is brittle and as such is best effort
warning = "Cannot verify that installed Java version at '{}' \
is correct.".format(
java_path
)
java_version_status_queue.put(warning)
def _try_to_verify_java_version_off_thread(
java_version_status_queue, java_path, required_java_version
):
""" Attempts to validate the java version off main execution thread.
The reason for this is to speed up the start-up time for the buck process.
testing has shown that starting java process is rather expensive and on local tests,
this optimization has reduced startup time of 'buck run' from 673 ms to 520 ms. """
verify_java_version_thread = threading.Thread(
target=_try_to_verify_java_version,
args=(java_version_status_queue, java_path, required_java_version),
)
verify_java_version_thread.daemon = True
verify_java_version_thread.start()
def _emit_java_version_warnings_if_any(java_version_status_queue):
""" Emits java_version warnings that got posted in the java_version_status_queue
queus from the java version verification thread.
There are 2 cases where we need to take special care for.
1. The main thread finishes before the main thread gets here before the version testing
thread is done. In such case we wait for 50 ms. This should pretty much never happen,
except in cases where buck deployment or the VM is really badly misconfigured.
2. The java version thread never testing returns. This can happen if the process that is
called java is hanging for some reason. This is also not a normal case, and in such case
we will wait for 50 ms and if still no response, ignore the error."""
if java_version_status_queue.empty():
time.sleep(0.05)
if not java_version_status_queue.empty():
warning = java_version_status_queue.get()
if warning is not None:
logging.warning(warning)
def main(argv, reporter):
# Change environment at startup to ensure we don't have any other threads
# running yet.
# We set BUCK_ROOT_BUILD_ID to ensure that if we're called in a nested fashion
# from, say, a genrule, we do not reuse the UUID, and logs do not end up with
# confusing / incorrect data
# TODO: remove ability to inject BUCK_BUILD_ID completely. It mostly causes
# problems, and is not a generally useful feature for users.
if "BUCK_BUILD_ID" in os.environ and "BUCK_ROOT_BUILD_ID" not in os.environ:
build_id = os.environ["BUCK_BUILD_ID"]
else:
build_id = str(uuid.uuid4())
if "BUCK_ROOT_BUILD_ID" not in os.environ:
os.environ["BUCK_ROOT_BUILD_ID"] = build_id
java_version_status_queue = Queue(maxsize=1)
def get_repo(p):
# Try to detect if we're running a PEX by checking if we were invoked
# via a zip file.
if zipfile.is_zipfile(argv[0]):
from programs.buck_package import BuckPackage
return BuckPackage(p, reporter)
else:
from programs.buck_repo import BuckRepo
return BuckRepo(THIS_DIR, p, reporter)
def kill_buck(reporter):
buck_repo = get_repo(BuckProject.from_current_dir())
buck_repo.kill_buckd()
return ExitCode.SUCCESS
# Execute wrapper specific commands
wrapper_specific_commands = [("kill", kill_buck), ("killall", killall_buck)]
if "--help" not in argv and "-h" not in argv:
for command_str, command_fcn in wrapper_specific_commands:
if len(argv) > 1 and argv[1] == command_str:
return ExitCodeCallable(command_fcn(reporter))
install_signal_handlers()
try:
tracing_dir = None
reporter.build_id = build_id
with Tracing("main"):
with BuckProject.from_current_dir() as project:
tracing_dir = os.path.join(project.get_buck_out_log_dir(), "traces")
with get_repo(project) as buck_repo:
required_java_version = buck_repo.get_buck_compiled_java_version()
java_path = get_java_path(required_java_version)
_try_to_verify_java_version_off_thread(
java_version_status_queue, java_path, required_java_version
)
return buck_repo.launch_buck(build_id, os.getcwd(), java_path, argv)
finally:
if tracing_dir:
Tracing.write_to_dir(tracing_dir, build_id)
_emit_java_version_warnings_if_any(java_version_status_queue)
if __name__ == "__main__":
exit_code = ExitCode.SUCCESS
reporter = BuckStatusReporter(sys.argv)
exit_code_callable = None
exception = None
exc_type = None
exc_traceback = None
try:
setup_logging()
exit_code_callable = main(sys.argv, reporter)
# Grab the original exit code here for logging. If this callable does something
# more advanced (like exec) we want to make sure that at least the original
# code is logged
exit_code = exit_code_callable.exit_code
except NoBuckConfigFoundException as e:
exception = e
# buck is started outside project root
exit_code = ExitCode.COMMANDLINE_ERROR
except BuckDaemonErrorException:
reporter.status_message = "Buck daemon disconnected unexpectedly"
_, exception, _ = sys.exc_info()
print(str(exception))
exception = None
exit_code = ExitCode.FATAL_GENERIC
except IOError as e:
exc_type, exception, exc_traceback = sys.exc_info()
if e.errno == errno.ENOSPC:
exit_code = ExitCode.FATAL_DISK_FULL
elif e.errno == errno.EPIPE:
exit_code = ExitCode.SIGNAL_PIPE
else:
exit_code = ExitCode.FATAL_IO
except KeyboardInterrupt:
reporter.status_message = "Python wrapper keyboard interrupt"
exit_code = ExitCode.SIGNAL_INTERRUPT
except Exception:
exc_type, exception, exc_traceback = sys.exc_info()
exit_code = ExitCode.FATAL_BOOTSTRAP
if exception is not None:
# If exc_info is non-None, a stacktrace is printed out, which we don't always
# want, but we want the exception data for the reporter
exc_info = None
if exc_type and exc_traceback:
exc_info = (exc_type, exception, exc_traceback)
logging.error(exception, exc_info=exc_info)
if reporter.status_message is None:
reporter.status_message = str(exception)
# report result of Buck call
try:
reporter.report(exit_code)
except Exception as e:
logging.debug(
"Exception occurred while reporting build results. This error is "
"benign and doesn't affect the actual build.",
exc_info=True,
)
# execute 'buck run' target
if exit_code_callable is not None:
exit_code = exit_code_callable()
propagate_failure(exit_code)