xref: /aosp_15_r20/external/autotest/autotest_lib/client/setup_modules.py (revision 9c5db1993ded3edbeafc8092d69fe5de2ee02df7)
1*9c5db199SXin Liimport os
2*9c5db199SXin Liimport re
3*9c5db199SXin Liimport six
4*9c5db199SXin Liimport sys
5*9c5db199SXin Li
6*9c5db199SXin Li# This must run on Python versions less than 2.4.
7*9c5db199SXin Lidirname = os.path.dirname(sys.modules[__name__].__file__)
8*9c5db199SXin Licommon_dir = os.path.abspath(os.path.join(dirname, 'common_lib'))
9*9c5db199SXin Lisys.path.insert(0, common_dir)
10*9c5db199SXin Liimport check_version
11*9c5db199SXin Lisys.path.pop(0)
12*9c5db199SXin Li
13*9c5db199SXin Li
14*9c5db199SXin Lidef _get_pyversion_from_args():
15*9c5db199SXin Li    """Extract, format, & pop the current py_version from args, if provided."""
16*9c5db199SXin Li    py_version = 3
17*9c5db199SXin Li    py_version_re = re.compile(r'--py_version=(\w+)\b')
18*9c5db199SXin Li
19*9c5db199SXin Li    version_found = False
20*9c5db199SXin Li    for i, arg in enumerate(sys.argv):
21*9c5db199SXin Li        if not arg.startswith('--py_version'):
22*9c5db199SXin Li            continue
23*9c5db199SXin Li        result = py_version_re.search(arg)
24*9c5db199SXin Li        if result:
25*9c5db199SXin Li            if version_found:
26*9c5db199SXin Li                raise ValueError('--py_version may only be specified once.')
27*9c5db199SXin Li            py_version = result.group(1)
28*9c5db199SXin Li            version_found = True
29*9c5db199SXin Li            if py_version not in ('2', '3'):
30*9c5db199SXin Li                raise ValueError('Python version must be "2" or "3".')
31*9c5db199SXin Li
32*9c5db199SXin Li            # Remove the arg so other argparsers don't get grumpy.
33*9c5db199SXin Li            sys.argv.pop(i)
34*9c5db199SXin Li
35*9c5db199SXin Li    return py_version
36*9c5db199SXin Li
37*9c5db199SXin Li
38*9c5db199SXin Lidef _desired_version():
39*9c5db199SXin Li    """
40*9c5db199SXin Li    Returns desired python version.
41*9c5db199SXin Li
42*9c5db199SXin Li    If the PY_VERSION env var is set, just return that. This is the case
43*9c5db199SXin Li    when autoserv kicks of autotest on the server side via a job.run(), or
44*9c5db199SXin Li    a process created a subprocess.
45*9c5db199SXin Li
46*9c5db199SXin Li    Otherwise, parse & pop the sys.argv for the '--py_version' flag. If no
47*9c5db199SXin Li    flag is set, default to python 3.
48*9c5db199SXin Li
49*9c5db199SXin Li    """
50*9c5db199SXin Li    # Even if the arg is in the env vars, we will attempt to get it from the
51*9c5db199SXin Li    # args, so that it can be popped prior to other argparsers hitting.
52*9c5db199SXin Li    py_version = _get_pyversion_from_args()
53*9c5db199SXin Li
54*9c5db199SXin Li    if os.getenv('PY_VERSION'):
55*9c5db199SXin Li        return int(os.getenv('PY_VERSION'))
56*9c5db199SXin Li
57*9c5db199SXin Li    os.environ['PY_VERSION'] = str(py_version)
58*9c5db199SXin Li    return int(py_version)
59*9c5db199SXin Li
60*9c5db199SXin Li
61*9c5db199SXin Lidesired_version = _desired_version()
62*9c5db199SXin Liif desired_version == sys.version_info.major:
63*9c5db199SXin Li    os.environ['AUTOTEST_NO_RESTART'] = 'True'
64*9c5db199SXin Lielse:
65*9c5db199SXin Li    # There are cases were this can be set (ie by test_that), but a subprocess
66*9c5db199SXin Li    # is launched in the incorrect version.
67*9c5db199SXin Li    if os.getenv('AUTOTEST_NO_RESTART'):
68*9c5db199SXin Li        del os.environ['AUTOTEST_NO_RESTART']
69*9c5db199SXin Li    check_version.check_python_version(desired_version)
70*9c5db199SXin Li
71*9c5db199SXin Liimport glob, traceback
72*9c5db199SXin Li
73*9c5db199SXin Li
74*9c5db199SXin Lidef import_module(module, from_where):
75*9c5db199SXin Li    """Equivalent to 'from from_where import module'
76*9c5db199SXin Li    Returns the corresponding module"""
77*9c5db199SXin Li    from_module = __import__(from_where, globals(), locals(), [module])
78*9c5db199SXin Li    return getattr(from_module, module)
79*9c5db199SXin Li
80*9c5db199SXin Li
81*9c5db199SXin Lidef _autotest_logging_handle_error(self, record):
82*9c5db199SXin Li    """Method to monkey patch into logging.Handler to replace handleError."""
83*9c5db199SXin Li    # The same as the default logging.Handler.handleError but also prints
84*9c5db199SXin Li    # out the original record causing the error so there is -some- idea
85*9c5db199SXin Li    # about which call caused the logging error.
86*9c5db199SXin Li    import logging
87*9c5db199SXin Li    if logging.raiseExceptions:
88*9c5db199SXin Li        # Avoid recursion as the below output can end up back in here when
89*9c5db199SXin Li        # something has *seriously* gone wrong in autotest.
90*9c5db199SXin Li        logging.raiseExceptions = 0
91*9c5db199SXin Li        sys.stderr.write('Exception occurred formatting message: '
92*9c5db199SXin Li                         '%r using args %r\n' % (record.msg, record.args))
93*9c5db199SXin Li        traceback.print_stack()
94*9c5db199SXin Li        sys.stderr.write('-' * 50 + '\n')
95*9c5db199SXin Li        traceback.print_exc()
96*9c5db199SXin Li        sys.stderr.write('Future logging formatting exceptions disabled.\n')
97*9c5db199SXin Li
98*9c5db199SXin Li
99*9c5db199SXin Lidef _monkeypatch_logging_handle_error():
100*9c5db199SXin Li    # Hack out logging.py*
101*9c5db199SXin Li    logging_py = os.path.join(os.path.dirname(__file__), 'common_lib',
102*9c5db199SXin Li                              'logging.py*')
103*9c5db199SXin Li    if glob.glob(logging_py):
104*9c5db199SXin Li        os.system('rm -f %s' % logging_py)
105*9c5db199SXin Li
106*9c5db199SXin Li    # Monkey patch our own handleError into the logging module's StreamHandler.
107*9c5db199SXin Li    # A nicer way of doing this -might- be to have our own logging module define
108*9c5db199SXin Li    # an autotest Logger instance that added our own Handler subclass with this
109*9c5db199SXin Li    # handleError method in it.  But that would mean modifying tons of code.
110*9c5db199SXin Li    import logging
111*9c5db199SXin Li    assert callable(logging.Handler.handleError)
112*9c5db199SXin Li    logging.Handler.handleError = _autotest_logging_handle_error
113*9c5db199SXin Li
114*9c5db199SXin Li
115*9c5db199SXin Lidef _insert_site_packages(root):
116*9c5db199SXin Li    # Allow locally installed third party packages to be found
117*9c5db199SXin Li    # before any that are installed on the system itself when not.
118*9c5db199SXin Li    # running as a client.
119*9c5db199SXin Li    # This is primarily for the benefit of frontend and tko so that they
120*9c5db199SXin Li    # may use libraries other than those available as system packages.
121*9c5db199SXin Li    if six.PY2:
122*9c5db199SXin Li        sys.path.insert(0, os.path.join(root, 'site-packages'))
123*9c5db199SXin Li
124*9c5db199SXin Li
125*9c5db199SXin Liimport importlib
126*9c5db199SXin Li
127*9c5db199SXin LiROOT_MODULE_NAME_ALLOW_LIST = (
128*9c5db199SXin Li        'autotest_lib',
129*9c5db199SXin Li        'autotest_lib.client',
130*9c5db199SXin Li)
131*9c5db199SXin Li
132*9c5db199SXin Li
133*9c5db199SXin Lidef _setup_top_level_symlink(base_path):
134*9c5db199SXin Li    """Create a self pointing symlink in the base_path)."""
135*9c5db199SXin Li    if os.path.islink(os.path.join(base_path, 'autotest_lib')):
136*9c5db199SXin Li        return
137*9c5db199SXin Li    os.chdir(base_path)
138*9c5db199SXin Li    os.symlink('.', 'autotest_lib')
139*9c5db199SXin Li
140*9c5db199SXin Li
141*9c5db199SXin Lidef _setup_client_symlink(base_path):
142*9c5db199SXin Li    """Setup the client symlink for the DUT.
143*9c5db199SXin Li
144*9c5db199SXin Li    Creates a "autotest_lib" folder in client, then creates a symlink called
145*9c5db199SXin Li    "client" pointing back to ../, as well as an __init__ for the folder.
146*9c5db199SXin Li    """
147*9c5db199SXin Li
148*9c5db199SXin Li    def _create_client_symlink():
149*9c5db199SXin Li        os.chdir(autotest_lib_dir)
150*9c5db199SXin Li        with open('__init__.py', 'w'):
151*9c5db199SXin Li            pass
152*9c5db199SXin Li        os.symlink('../', 'client')
153*9c5db199SXin Li
154*9c5db199SXin Li    autotest_lib_dir = os.path.join(base_path, 'autotest_lib')
155*9c5db199SXin Li    link_path = os.path.join(autotest_lib_dir, 'client')
156*9c5db199SXin Li
157*9c5db199SXin Li    # TODO: Use os.makedirs(..., exist_ok=True) after switching to Python 3
158*9c5db199SXin Li    if not os.path.isdir(autotest_lib_dir):
159*9c5db199SXin Li        try:
160*9c5db199SXin Li            os.mkdir(autotest_lib_dir)
161*9c5db199SXin Li        except FileExistsError as e:
162*9c5db199SXin Li            if not os.path.isdir(autotest_lib_dir):
163*9c5db199SXin Li                raise e
164*9c5db199SXin Li
165*9c5db199SXin Li    if os.path.islink(link_path):
166*9c5db199SXin Li        return
167*9c5db199SXin Li
168*9c5db199SXin Li    try:
169*9c5db199SXin Li        _create_client_symlink()
170*9c5db199SXin Li    # It's possible 2 autotest processes are running at once, and one
171*9c5db199SXin Li    # creates the symlink in the time between checking and creating.
172*9c5db199SXin Li    # Thus if the symlink DNE, and we cannot create it, check for its
173*9c5db199SXin Li    # existence and exit if it exists.
174*9c5db199SXin Li    except FileExistsError as e:
175*9c5db199SXin Li        if os.path.islink(link_path):
176*9c5db199SXin Li            return
177*9c5db199SXin Li        raise e
178*9c5db199SXin Li
179*9c5db199SXin Li
180*9c5db199SXin Lidef _symlink_check(base_path, root_dir):
181*9c5db199SXin Li    """Verify the required symlinks are present, and add them if not."""
182*9c5db199SXin Li    # Note the starting cwd to later change back to it.
183*9c5db199SXin Li    starting_dir = os.getcwd()
184*9c5db199SXin Li    if root_dir == 'autotest_lib':
185*9c5db199SXin Li        _setup_top_level_symlink(base_path)
186*9c5db199SXin Li    elif root_dir == 'autotest_lib.client':
187*9c5db199SXin Li        _setup_client_symlink(base_path)
188*9c5db199SXin Li
189*9c5db199SXin Li    os.chdir(starting_dir)
190*9c5db199SXin Li
191*9c5db199SXin Li
192*9c5db199SXin Lidef setup(base_path, root_module_name):
193*9c5db199SXin Li    _symlink_check(base_path, root_module_name)
194*9c5db199SXin Li    if root_module_name not in ROOT_MODULE_NAME_ALLOW_LIST:
195*9c5db199SXin Li        raise Exception('Unexpected root module: ' + root_module_name)
196*9c5db199SXin Li
197*9c5db199SXin Li    _insert_site_packages(base_path)
198*9c5db199SXin Li
199*9c5db199SXin Li    # Ie, server (or just not /client)
200*9c5db199SXin Li    if root_module_name == 'autotest_lib':
201*9c5db199SXin Li        # Base path is just x/x/x/x/autotest/files
202*9c5db199SXin Li        _setup_autotest_lib(base_path)
203*9c5db199SXin Li        _preimport_top_level_packages(os.path.join(base_path, 'autotest_lib'),
204*9c5db199SXin Li                                      parent='autotest_lib')
205*9c5db199SXin Li    else:  # aka, in /client/
206*9c5db199SXin Li        if os.path.exists(os.path.join(os.path.dirname(base_path), 'server')):
207*9c5db199SXin Li
208*9c5db199SXin Li            # Takes you from /client/ to /files
209*9c5db199SXin Li            # this is because on DUT there is no files/client
210*9c5db199SXin Li            autotest_base_path = os.path.dirname(base_path)
211*9c5db199SXin Li
212*9c5db199SXin Li        else:
213*9c5db199SXin Li            autotest_base_path = base_path
214*9c5db199SXin Li
215*9c5db199SXin Li        _setup_autotest_lib(autotest_base_path)
216*9c5db199SXin Li        _preimport_top_level_packages(os.path.join(autotest_base_path,
217*9c5db199SXin Li                                                   'autotest_lib'),
218*9c5db199SXin Li                                      parent='autotest_lib')
219*9c5db199SXin Li        _preimport_top_level_packages(
220*9c5db199SXin Li                os.path.join(autotest_base_path, 'autotest_lib', 'client'),
221*9c5db199SXin Li                parent='autotest_lib.client',
222*9c5db199SXin Li        )
223*9c5db199SXin Li
224*9c5db199SXin Li    _monkeypatch_logging_handle_error()
225*9c5db199SXin Li
226*9c5db199SXin Li
227*9c5db199SXin Lidef _setup_autotest_lib(path):
228*9c5db199SXin Li    sys.path.insert(0, path)
229*9c5db199SXin Li    # This is a symlink back to the root directory, that does all the magic.
230*9c5db199SXin Li    importlib.import_module('autotest_lib')
231*9c5db199SXin Li    sys.path.pop(0)
232*9c5db199SXin Li
233*9c5db199SXin Li
234*9c5db199SXin Lidef _preimport_top_level_packages(root, parent):
235*9c5db199SXin Li    # The old code to setup the packages used to fetch the top-level packages
236*9c5db199SXin Li    # inside autotest_lib. We keep that behaviour in order to avoid having to
237*9c5db199SXin Li    # add import statements for the top-level packages all over the codebase.
238*9c5db199SXin Li    #
239*9c5db199SXin Li    # e.g.,
240*9c5db199SXin Li    #  import common
241*9c5db199SXin Li    #  from autotest_lib.server import utils
242*9c5db199SXin Li    #
243*9c5db199SXin Li    # must continue to work. The _right_ way to do that import would be.
244*9c5db199SXin Li    #
245*9c5db199SXin Li    #  import common
246*9c5db199SXin Li    #  import autotest_lib.server
247*9c5db199SXin Li    #  from autotest_lib.server import utils
248*9c5db199SXin Li    names = []
249*9c5db199SXin Li    for filename in os.listdir(root):
250*9c5db199SXin Li        path = os.path.join(root, filename)
251*9c5db199SXin Li        if not os.path.isdir(path):
252*9c5db199SXin Li            continue  # skip files
253*9c5db199SXin Li        if '.' in filename:
254*9c5db199SXin Li            continue  # if "." is in the name it's not a valid package name
255*9c5db199SXin Li        if not os.access(path, os.R_OK | os.X_OK):
256*9c5db199SXin Li            continue  # need read + exec access to make a dir importable
257*9c5db199SXin Li        if '__init__.py' in os.listdir(path):
258*9c5db199SXin Li            names.append(filename)
259*9c5db199SXin Li
260*9c5db199SXin Li    for name in names:
261*9c5db199SXin Li        pname = parent + '.' + name
262*9c5db199SXin Li        importlib.import_module(pname)
263*9c5db199SXin Li        if name != 'autotest_lib':
264*9c5db199SXin Li            sys.modules[name] = sys.modules[pname]
265