2
# -*- coding: UTF-8 -*-
5
# Copyright © 2010 Piotr Ożarowski <piotr@debian.org>
7
# Permission is hereby granted, free of charge, to any person obtaining a copy
8
# of this software and associated documentation files (the "Software"), to deal
9
# in the Software without restriction, including without limitation the rights
10
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11
# copies of the Software, and to permit persons to whom the Software is
12
# furnished to do so, subject to the following conditions:
14
# The above copyright notice and this permission notice shall be included in
15
# all copies or substantial portions of the Software.
17
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
25
from __future__ import with_statement
29
from os import environ, listdir, walk
30
from os.path import abspath, exists, isdir, isfile, join
31
from subprocess import PIPE, Popen
32
sys.path.insert(1, '/usr/share/python/')
33
from debpython.version import SUPPORTED, debsorted, vrepr, \
34
get_requested_versions, parse_vrange, getver
35
from debpython.option import Option, compile_regexpr
36
from debpython.pydist import PUBLIC_DIR_RE
37
from debpython.tools import memoize
40
logging.basicConfig(format='%(levelname).1s: %(module)s:%(lineno)d: '
42
log = logging.getLogger(__name__)
46
"""TODO: move it to manpage
48
pycompile -p python-mako # package's public files
49
pycompile -p foo /usr/share/foo # package's private files
50
pycompile -p foo -V 2.6- /usr/share/foo # private files, Python >= 2.6
51
pycompile -V 2.6 /usr/lib/python2.6/dist-packages # python2.6 only
52
pycompile -V 2.6 /usr/lib/foo/bar.py # python2.6 only
56
### FILES ######################################################
57
def get_directory_files(dname):
58
"""Generate *.py file names available in given directory."""
59
if isfile(dname) and dname.endswith('.py'):
62
for root, dirs, file_names in walk(abspath(dname)):
63
#if root != dname and not exists(join(root, '__init__.py')):
67
if fn.endswith('.py'):
71
def get_package_files(package_name):
72
"""Generate *.py file names available in given package."""
73
process = Popen("/usr/bin/dpkg -L %s" % package_name,\
74
shell=True, stdout=PIPE)
75
stdout, stderr = process.communicate()
76
if process.returncode != 0:
77
log.error('cannot get content of %s', package_name)
79
for line in stdout.split('\n'):
80
if line.endswith('.py'):
84
def get_private_files(files, dname):
85
"""Generate *.py file names that match given directory."""
87
if fn.startswith(dname):
91
def get_public_files(files, versions):
92
"""Generate *.py file names that match given versions."""
93
versions_str = set("%d.%d" % i for i in versions)
95
if fn.startswith('/usr/lib/python') and \
96
fn[15:18] in versions_str:
100
### EXCLUDES ###################################################
102
def get_exclude_patterns_from_dir(name='/usr/share/python/bcep/'):
103
"""Return patterns for files that shouldn't be bytecompiled."""
108
for fn in listdir(name):
109
with file(join(name, fn), 'r') as lines:
111
type_, vrange, dname, pattern = line.split('|', 3)
112
vrange = parse_vrange(vrange)
113
versions = get_requested_versions(vrange, available=True)
115
# pattern doesn't match installed Python versions
117
pattern = pattern.rstrip('\n')
119
pattern = compile_regexpr(None, None, pattern)
120
result.append((type_, versions, dname, pattern))
124
def get_exclude_patterns(directory='/', patterns=None, versions=None):
125
"""Return patterns for files that shouldn't be compiled in given dir."""
128
versions = set(SUPPORTED)
129
patterns = [('re', versions, directory, i) for i in patterns]
133
for type_, vers, dname, pattern in get_exclude_patterns_from_dir():
134
# skip patterns that do not match requested directory
135
if not dname.startswith(directory[:len(dname)]):
137
# skip patterns that do not match requested versions
138
if versions and not versions & vers:
140
patterns.append((type_, vers, dname, pattern))
144
def filter_files(files, e_patterns, compile_versions):
145
"""Generate (file, versions_to_compile) pairs."""
147
valid_versions = set(compile_versions) # all by default
149
for type_, vers, dname, pattern in e_patterns:
150
if type_ == 'dir' and fn.startswith(dname):
151
valid_versions = valid_versions - vers
152
elif type_ == 're' and pattern.match(fn):
153
valid_versions = valid_versions - vers
155
# move to the next file if all versions were removed
156
if not valid_versions:
159
public_dir = PUBLIC_DIR_RE.match(fn)
161
yield fn, set([getver(public_dir.group(1))])
163
yield fn, valid_versions
166
### COMPILE ####################################################
167
def py_compile(version, workers):
168
if not isinstance(version, basestring):
169
version = vrepr(version)
170
cmd = "python%s -m py_compile -" % version
171
process = Popen(cmd, bufsize=1, shell=True,
172
stdin=PIPE, close_fds=True)
173
workers[version] = process # keep the reference for .communicate()
174
stdin = process.stdin
177
stdin.write(filename + '\n')
180
def compile(files, versions, e_patterns=None):
181
global STDINS, WORKERS
182
# start Python interpreters that will handle byte compilation
183
for version in versions:
184
if version not in STDINS:
185
coroutine = py_compile(version, WORKERS)
187
STDINS[version] = coroutine
190
for fn, versions_to_compile in filter_files(files, e_patterns, versions):
191
if exists("%sc" % fn):
193
for version in versions_to_compile:
194
pipe = STDINS[version]
198
################################################################
200
usage = '%prog [-V [X.Y][-][A.B]] DIR_OR_FILE [-X REGEXPR]\n' + \
202
parser = optparse.OptionParser(usage, version='%prog 0.9',
204
parser.add_option('-v', '--verbose', action='store_true', dest='verbose',
205
help='turn verbose mode on')
206
parser.add_option('-q', '--quiet', action='store_false', dest='verbose',
207
default=False, help='be quiet')
208
parser.add_option('-p', '--package',
209
help='specify Debian package name whose files should be bytecompiled')
210
parser.add_option('-V', type='version_range', dest='vrange',
211
help="""force private modules to be bytecompiled with Python version
212
from given range, regardless of the default Python version in the system.
213
If there are no other options, bytecompile all public modules for installed
214
Python versions that match given range.
216
VERSION_RANGE examples: '2.5' (version 2.5 only), '2.5-' (version 2.5 or
217
newer), '2.5-2.7' (version 2.5 or 2.6), '-3.0' (all supported 2.X versions)""")
218
parser.add_option('-X', '--exclude', action='append',
219
dest='regexpr', type='regexpr',
220
help='exclude items that match given REGEXPR. You may use this option \
221
multiple times to build up a list of things to exclude.')
223
(options, args) = parser.parse_args()
225
if options.verbose or environ.get('PYCOMPILE_DEBUG') == '1':
226
log.setLevel(logging.DEBUG)
227
log.debug('argv: %s', sys.argv)
228
log.debug('options: %s', options)
229
log.debug('args: %s', args)
231
log.setLevel(logging.WARN)
233
if options.regexpr and not args:
234
parser.error('--exclude option works with private directories '
235
'only, please use /usr/share/python/bcep to specify '
236
'public modules to skip')
238
versions = get_requested_versions(options.vrange, available=True)
240
log.error('Requested versions are not installed')
243
if options.package and args: # package's private directories
244
# get requested Python version
245
compile_versions = debsorted(versions)[:1]
246
log.debug('compile versions: %s', versions)
248
pkg_files = tuple(get_package_files(options.package))
250
e_patterns = get_exclude_patterns(item, options.regexpr, \
253
log.warn('No such file or directory: %s', item)
255
log.debug('byte compiling %s using Python %s',
256
item, compile_versions)
257
files = get_private_files(pkg_files, item)
258
compile(files, compile_versions, e_patterns)
259
elif options.package: # package's public modules
260
# no need to limit versions here, it's either pyr mode or version is
261
# hardcoded in path / via -V option
262
e_patterns = get_exclude_patterns()
263
files = get_package_files(options.package)
264
files = get_public_files(files, versions)
265
compile(files, versions, e_patterns)
266
elif args: # other directories/files (public ones mostly)
267
versions = debsorted(versions)[:1]
269
e_patterns = get_exclude_patterns(item, options.regexpr, versions)
270
files = get_directory_files(item)
271
compile(files, versions, e_patterns)
276
# wait for all processes to finish
277
for process in WORKERS.itervalues():
278
process.communicate()
280
if __name__ == '__main__':