blob: 8a8a808380d344808db7733b256cf21e3841f2e9 [file] [log] [blame]
Mike Frysinger1c57dfe2020-02-06 00:28:22 -05001#!/usr/bin/env python3
2# -*- coding: utf-8 -*-
3# Copyright 2020 The Chromium OS Authors. All rights reserved.
4# Use of this source code is governed by a BSD-style license that can be
5# found in the LICENSE file.
6
7"""Wrapper around chromite executable scripts.
8
9This takes care of creating a consistent environment for chromite scripts
10(like setting up import paths) so we don't have to duplicate the logic in
11lots of places.
12"""
13
14from __future__ import print_function
15
16import importlib
17import os
18import sys
19
20
21# Assert some minimum Python versions as we don't test or support any others.
22# We only support Python 3.6+.
Mike Frysinger54545bc2020-02-16 05:37:56 +000023if sys.version_info < (3, 6):
24 print('%s: chromite: error: Python-3.6+ is required' % (sys.argv[0],),
Mike Frysinger1c57dfe2020-02-06 00:28:22 -050025 file=sys.stderr)
26 sys.exit(1)
Mike Frysinger1c57dfe2020-02-06 00:28:22 -050027
28
29CHROMITE_PATH = None
30
31
32class ChromiteImporter(object):
33 """Virtual chromite module
34
35 If the checkout is not named 'chromite', trying to do 'from chromite.xxx'
36 to import modules fails horribly. Instead, manually locate the chromite
37 directory (whatever it is named), load & return it whenever someone tries
38 to import it. This lets us use the stable name 'chromite' regardless of
39 how things are structured on disk.
40
41 This also lets us keep the sys.path search clean. Otherwise we'd have to
42 worry about what other dirs chromite were checked out near to as doing an
43 import would also search those for .py modules.
44 """
45
46 # When trying to load the chromite dir from disk, we'll get called again,
47 # so make sure to disable our logic to avoid an infinite loop.
48 _loading = False
49
50 def find_module(self, fullname, _path=None):
51 """Handle the 'chromite' module"""
52 if fullname == 'chromite' and not self._loading:
53 return self
54 return None
55
56 def load_module(self, _fullname):
57 """Return our cache of the 'chromite' module"""
58 # Locate the top of the chromite dir by searching for the PRESUBMIT.cfg
59 # file. This assumes that file isn't found elsewhere in the tree.
60 path = os.path.dirname(os.path.realpath(__file__))
61 while not os.path.exists(os.path.join(path, 'PRESUBMIT.cfg')):
62 path = os.path.dirname(path)
63
64 # pylint: disable=global-statement
65 global CHROMITE_PATH
66 CHROMITE_PATH = path + '/'
67
68 # Finally load the chromite dir.
69 path, mod = os.path.split(path)
70 sys.path.insert(0, path)
71 self._loading = True
72 try:
73 return importlib.import_module(mod)
74 finally:
75 # We can't pop by index as the import might have changed sys.path.
76 sys.path.remove(path)
77 self._loading = False
78
79
80sys.meta_path.insert(0, ChromiteImporter())
81
82# We have to put these imports after our meta-importer above.
83# pylint: disable=wrong-import-position
84from chromite.lib import commandline
85
86
87def FindTarget(target):
88 """Turn the path into something we can import from the chromite tree.
89
90 This supports a variety of ways of running chromite programs:
91 # Loaded via depot_tools in $PATH.
92 $ cros_sdk --help
93 # Loaded via .../chromite/bin in $PATH.
94 $ cros --help
95 # No $PATH needed.
96 $ ./bin/cros --help
97 # Loaded via ~/bin in $PATH to chromite bin/ subdir.
98 $ ln -s $PWD/bin/cros ~/bin; cros --help
99 # No $PATH needed.
100 $ ./cbuildbot/cbuildbot --help
101 # No $PATH needed, but symlink inside of chromite dir.
102 $ ln -s ./cbuildbot/cbuildbot; ./cbuildbot --help
103 # Loaded via ~/bin in $PATH to non-chromite bin/ subdir.
104 $ ln -s $PWD/cbuildbot/cbuildbot ~/bin/; cbuildbot --help
105 # No $PATH needed, but a relative symlink to a symlink to the chromite dir.
106 $ cd ~; ln -s bin/cbuildbot ./; ./cbuildbot --help
107 # External chromite module
108 $ ln -s ../chromite/scripts/wrapper.py foo; ./foo
109
110 Args:
111 target: Path to the script we're trying to run.
112
113 Returns:
114 The module main functor.
115 """
116 # We assume/require the script we're wrapping ends in a .py.
117 full_path = target + '.py'
118 while True:
119 # Walk back one symlink at a time until we get into the chromite dir.
120 parent, base = os.path.split(target)
121 parent = os.path.realpath(parent)
122 if parent.startswith(CHROMITE_PATH):
123 target = base
124 break
125 target = os.path.join(os.path.dirname(target), os.readlink(target))
126
127 # If we walked all the way back to wrapper.py, it means we're trying to run
128 # an external module. So we have to import it by filepath and not via the
129 # chromite.xxx.yyy namespace.
130 if target != 'wrapper3.py':
131 assert parent.startswith(CHROMITE_PATH), (
132 'could not figure out leading path\n'
133 '\tparent: %s\n'
134 '\tCHROMITE_PATH: %s' % (parent, CHROMITE_PATH))
135 parent = parent[len(CHROMITE_PATH):].split(os.sep)
136 target = ['chromite'] + parent + [target]
137
138 if target[1] == 'bin':
139 # Convert chromite/bin/foo -> chromite/scripts/foo.
140 # Since chromite/bin/ is in $PATH, we want to keep it clean.
141 target[1] = 'scripts'
Mike Frysinger1c57dfe2020-02-06 00:28:22 -0500142
143 try:
144 module = importlib.import_module('.'.join(target))
145 except ImportError as e:
146 print(
147 '%s: could not import chromite module: %s: %s' % (sys.argv[0],
148 full_path, e),
149 file=sys.stderr)
150 raise
151 else:
152 import types
153 try:
154 loader = importlib.machinery.SourceFileLoader('main', full_path)
155 module = types.ModuleType(loader.name)
156 loader.exec_module(module)
157 except IOError as e:
158 print(
159 '%s: could not import external module: %s: %s' % (sys.argv[0],
160 full_path, e),
161 file=sys.stderr)
162 raise
163
164 # Run the module's main func if it has one.
165 main = getattr(module, 'main', None)
166 if main:
167 return main
168
169 # Is this a unittest?
170 if target[-1].rsplit('_', 1)[-1] in ('test', 'unittest'):
171 from chromite.lib import cros_test_lib
172 return lambda _argv: cros_test_lib.main(module=module)
173
174
175def DoMain():
176 commandline.ScriptWrapperMain(FindTarget)
177
178
179if __name__ == '__main__':
180 DoMain()