blob: 64606623ac69563ba19bf6d17f7c893d70e56496 [file] [log] [blame]
#!/usr/bin/python -b
# Copyright 2011-2014 Gentoo Foundation
# Distributed under the terms of the GNU General Public License v2
"""Helper tool for converting installed files to custom prefixes.
In other words, eprefixy $D for Gentoo/Prefix."""
import io
import os
import stat
import sys
from portage.util._argparse import ArgumentParser
# Argument parsing compatibility for Python 2.6 using optparse.
if sys.hexversion < 0x2070000:
from optparse import OptionParser
from optparse import OptionError
CONTENT_ENCODING = 'utf_8'
FS_ENCODING = 'utf_8'
try:
import magic
except ImportError:
magic = None
else:
try:
magic.MIME_TYPE
except AttributeError:
# magic module seems to be broken
magic = None
class IsTextFile(object):
def __init__(self):
if magic is not None:
self._call = self._is_text_magic
self._m = magic.open(magic.MIME_TYPE)
self._m.load()
else:
self._call = self._is_text_encoding
self._encoding = CONTENT_ENCODING
def __call__(self, filename):
"""
Returns True if the given file is a text file, and False otherwise.
"""
return self._call(filename)
def _is_text_magic(self, filename):
mime_type = self._m.file(filename)
if isinstance(mime_type, bytes):
mime_type = mime_type.decode('ascii', 'replace')
return mime_type.startswith('text/')
def _is_text_encoding(self, filename):
try:
for line in io.open(filename, mode='r', encoding=self._encoding):
pass
except UnicodeDecodeError:
return False
return True
def chpath_inplace(filename, is_text_file, old, new):
"""
Returns True if any modifications were made, and False otherwise.
"""
modified = False
orig_stat = os.lstat(filename)
try:
f = io.open(filename, buffering=0, mode='r+b')
except IOError:
try:
orig_mode = stat.S_IMODE(os.lstat(filename).st_mode)
except OSError as e:
sys.stderr.write('%s: %s\n' % (e, filename))
return
temp_mode = 0o200 | orig_mode
os.chmod(filename, temp_mode)
try:
f = io.open(filename, buffering=0, mode='r+b')
finally:
os.chmod(filename, orig_mode)
len_old = len(old)
len_new = len(new)
matched_byte_count = 0
while True:
in_byte = f.read(1)
if not in_byte:
break
if in_byte == old[matched_byte_count]:
matched_byte_count += 1
if matched_byte_count == len_old:
modified = True
matched_byte_count = 0
end_position = f.tell()
start_position = end_position - len_old
if not is_text_file:
# search backwards for leading slashes written by
# a previous invocation of this tool
num_to_write = len_old
f.seek(start_position - 1)
while True:
if f.read(1) != b'/':
break
num_to_write += 1
f.seek(f.tell() - 2)
# pad with as many leading slashes as necessary
while num_to_write > len_new:
f.write(b'/')
num_to_write -= 1
f.write(new)
else:
remainder = f.read()
f.seek(start_position)
f.write(new)
if remainder:
f.write(remainder)
f.truncate()
f.seek(start_position + len_new)
elif matched_byte_count > 0:
# back up an try to start a new match after
# the first byte of the previous partial match
f.seek(f.tell() - matched_byte_count)
matched_byte_count = 0
f.close()
if modified:
if sys.hexversion >= 0x3030000:
orig_mtime = orig_stat.st_mtime_ns
os.utime(filename, ns=(orig_mtime, orig_mtime))
else:
orig_mtime = orig_stat[stat.ST_MTIME]
os.utime(filename, (orig_mtime, orig_mtime))
return modified
def chpath_inplace_symlink(filename, st, old, new):
target = os.readlink(filename)
if target.startswith(old):
new_target = new + target[len(old):]
os.unlink(filename)
os.symlink(new_target, filename)
os.lchown(filename, st.st_uid, st.st_gid)
def main(argv):
parser = ArgumentParser(description=__doc__)
try:
parser.add_argument('location', default=None,
help='root directory (e.g. $D)')
parser.add_argument('old', default=None,
help='original build prefix (e.g. /)')
parser.add_argument('new', default=None,
help='new install prefix (e.g. $EPREFIX)')
opts = parser.parse_args(argv)
location, old, new = opts.location, opts.old, opts.new
except OptionError:
# Argument parsing compatibility for Python 2.6 using optparse.
if sys.hexversion < 0x2070000:
parser = OptionParser(description=__doc__,
usage="usage: %prog [-h] location old new\n\n" + \
" location: root directory (e.g. $D)\n" + \
" old: original build prefix (e.g. /)\n" + \
" new: new install prefix (e.g. $EPREFIX)")
(opts, args) = parser.parse_args()
if len(args) != 3:
parser.print_usage()
print("%s: error: expected 3 arguments, got %i"
% (__file__, len(args)))
return
location, old, new = args[0:3]
else:
raise
is_text_file = IsTextFile()
if not isinstance(location, bytes):
location = location.encode(FS_ENCODING)
if not isinstance(old, bytes):
old = old.encode(FS_ENCODING)
if not isinstance(new, bytes):
new = new.encode(FS_ENCODING)
st = os.lstat(location)
if stat.S_ISDIR(st.st_mode):
for parent, dirs, files in os.walk(location):
for filename in files:
filename = os.path.join(parent, filename)
try:
st = os.lstat(filename)
except OSError:
pass
else:
if stat.S_ISREG(st.st_mode):
chpath_inplace(filename,
is_text_file(filename), old, new)
elif stat.S_ISLNK(st.st_mode):
chpath_inplace_symlink(filename, st, old, new)
elif stat.S_ISREG(st.st_mode):
chpath_inplace(location,
is_text_file(location), old, new)
elif stat.S_ISLNK(st.st_mode):
chpath_inplace_symlink(location, st, old, new)
return os.EX_OK
if __name__ == '__main__':
sys.exit(main(sys.argv[1:]))