OILS / build / dynamic_deps.py View on Github | oilshell.org

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