OILS / build / dynamic_deps.py View on Github | oils.pub

163 lines, 95 significant
1# NO SHEBANG because we call it directly.
2"""
3app_deps.py
4
5Dynamically discover Python and C modules. We import the main module and
6inspect sys.modules before and after. That is, we use the exact logic that the
7Python interpreter does.
8
9Usage:
10 PYTHONPATH=... py_deps.py <main module>
11
12IMPORTANT: Run this script with -S so that system libraries aren't found.
13"""
14from __future__ import print_function
15
16import sys
17OLD_MODULES = dict(sys.modules) # Make a copy
18
19import posix # Do it afterward so we don't mess up analysis.
20
21VERBOSE = False
22#VERBOSE = True
23
24def log(msg, *args):
25 if not VERBOSE:
26 return
27 if args:
28 msg = msg % args
29 print('\t', msg, file=sys.stderr)
30
31
32def ImportMain(main_module, old_modules):
33 """Yields (module name, absolute path) pairs."""
34
35 log('Importing %r', main_module)
36 try:
37 __import__(main_module)
38 except ImportError as e:
39 log('Error importing %r with sys.path %r', main_module, sys.path)
40 # TODO: print better error.
41 raise
42
43 new_modules = sys.modules
44 log('After importing: %d modules', len(new_modules))
45
46 for name in sorted(new_modules):
47 if name in old_modules:
48 continue # exclude old modules
49
50 module = new_modules[name]
51
52 full_path = getattr(module, '__file__', None)
53
54 # For some reason, there are entries like:
55 # 'pan.core.os': None in sys.modules. Here's a hack to get rid of them.
56 if module is None:
57 log('module is None: %r', name)
58 continue
59 # Not sure why, but some stdlib modules don't have a __file__ attribute,
60 # e.g. "gc", "marshal", "thread". Doesn't matter for our purposes.
61 if full_path is None:
62 # _sre has this issue, because it's built-in
63 log('full_path is None: %r', name)
64 continue
65 yield name, full_path
66
67 # Special case for __future__. It's necessary, but doesn't get counted
68 # because we import it first!
69 module = sys.modules['__future__']
70 full_path = getattr(module, '__file__', None)
71 yield '__future__', full_path
72
73
74PY_MODULE = 0
75C_MODULE = 1
76
77
78def FilterModules(modules):
79 """Look at __file__ of each module, and classify them as Python or C."""
80
81 for module, full_path in modules:
82 #log('FilterModules %s %s', module, full_path)
83 num_parts = module.count('.') + 1
84 i = len(full_path)
85 # Do it once more in this case
86 if full_path.endswith('/__init__.pyc') or \
87 full_path.endswith('__init__.py'):
88 i = full_path.rfind('/', 0, i)
89 for _ in range(num_parts): # range for Python 3
90 i = full_path.rfind('/', 0, i)
91 #print i, full_path[i+1:]
92 rel_path = full_path[i + 1:]
93
94 # Depending on whether it's cached, the __file__ attribute on the module
95 # ends with '.py' or '.pyc'.
96 if full_path.endswith('.py'):
97 yield PY_MODULE, full_path, rel_path
98 elif full_path.endswith('.pyc'):
99 yield PY_MODULE, full_path[:-1], rel_path[:-1]
100 else:
101 # .so file
102 yield C_MODULE, module, full_path
103
104
105def main(argv):
106 """Returns an exit code."""
107
108 # Set an environment variable so dependencies in debug mode can be excluded.
109 posix.environ['_OVM_DEPS'] = '1'
110
111 action = argv[1]
112 main_module = argv[2]
113 log('Before importing: %d modules', len(OLD_MODULES))
114 log('OLD %s', OLD_MODULES.keys())
115
116 if action == 'both': # Write files for both .py and .so dependencies
117 prefix = argv[3]
118 py_out_path = prefix + '-cpython.txt'
119 c_out_path = prefix + '-c.txt'
120
121 modules = ImportMain(main_module, OLD_MODULES)
122 #log('NEW %s', list(modules))
123
124 with open(py_out_path, 'w') as py_out, open(c_out_path, 'w') as c_out:
125 for mod_type, x, y in FilterModules(modules):
126 if mod_type == PY_MODULE:
127 print(x, y, file=py_out)
128 print(x + 'c', y + 'c', file=py_out) # .pyc goes in bytecode.zip too
129
130 elif mod_type == C_MODULE:
131 print(x, y, file=c_out) # mod_name, full_path
132
133 else:
134 raise AssertionError(mod_type)
135
136 elif action == 'py': # .py path -> .pyc relative path
137 modules = ImportMain(main_module, OLD_MODULES)
138 for mod_type, full_path, rel_path in FilterModules(modules):
139 if mod_type == PY_MODULE:
140 opy_input = full_path
141 opy_output = rel_path + 'c' # output is .pyc
142 print(opy_input, opy_output)
143
144 elif action == 'py-manifest': # .py path -> .py relative path
145 modules = ImportMain(main_module, OLD_MODULES)
146 for mod_type, full_path, rel_path in FilterModules(modules):
147 if mod_type == PY_MODULE:
148 opy_input = full_path
149 assert rel_path.endswith('.py')
150 #mod_name = rel_path[:-3].replace('/', '.')
151 print(opy_input, rel_path)
152 else:
153 raise RuntimeError('Invalid action %r' % action)
154
155
156if __name__ == '__main__':
157 try:
158 sys.exit(main(sys.argv))
159 except RuntimeError as e:
160 print('%s: %s' % (sys.argv[0], e.args[0]), file=sys.stderr)
161 sys.exit(1)
162
163# vim: ts=2