Enable BytesWarnings.
[portage.git] / bin / chpathtool.py
1 #!/usr/bin/python -bb
2 # Copyright 2011-2014 Gentoo Foundation
3 # Distributed under the terms of the GNU General Public License v2
4
5 """Helper tool for converting installed files to custom prefixes.
6
7 In other words, eprefixy $D for Gentoo/Prefix."""
8
9 import io
10 import os
11 import stat
12 import sys
13
14 from portage.util._argparse import ArgumentParser
15
16 # Argument parsing compatibility for Python 2.6 using optparse.
17 if sys.hexversion < 0x2070000:
18         from optparse import OptionParser
19
20 from optparse import OptionError
21
22 CONTENT_ENCODING = 'utf_8'
23 FS_ENCODING = 'utf_8'
24
25 try:
26         import magic
27 except ImportError:
28         magic = None
29 else:
30         try:
31                 magic.MIME_TYPE
32         except AttributeError:
33                 # magic module seems to be broken
34                 magic = None
35
36 class IsTextFile(object):
37
38         def __init__(self):
39                 if magic is not None:
40                         self._call = self._is_text_magic
41                         self._m = magic.open(magic.MIME_TYPE)
42                         self._m.load()
43                 else:
44                         self._call = self._is_text_encoding
45                         self._encoding = CONTENT_ENCODING
46
47         def __call__(self, filename):
48                 """
49                 Returns True if the given file is a text file, and False otherwise.
50                 """
51                 return self._call(filename)
52
53         def _is_text_magic(self, filename):
54                 mime_type = self._m.file(filename)
55                 if isinstance(mime_type, bytes):
56                         mime_type = mime_type.decode('ascii', 'replace')
57                 return mime_type.startswith('text/')
58
59         def _is_text_encoding(self, filename):
60                 try:
61                         for line in io.open(filename, mode='r', encoding=self._encoding):
62                                 pass
63                 except UnicodeDecodeError:
64                         return False
65                 return True
66
67 def chpath_inplace(filename, is_text_file, old, new):
68         """
69         Returns True if any modifications were made, and False otherwise.
70         """
71
72         modified = False
73         orig_stat = os.lstat(filename)
74         try:
75                 f = io.open(filename, buffering=0, mode='r+b')
76         except IOError:
77                 try:
78                         orig_mode = stat.S_IMODE(os.lstat(filename).st_mode)
79                 except OSError as e:
80                         sys.stderr.write('%s: %s\n' % (e, filename))
81                         return
82                 temp_mode = 0o200 | orig_mode
83                 os.chmod(filename, temp_mode)
84                 try:
85                         f = io.open(filename, buffering=0, mode='r+b')
86                 finally:
87                         os.chmod(filename, orig_mode)
88
89         len_old = len(old)
90         len_new = len(new)
91         matched_byte_count = 0
92         while True:
93                 in_byte = f.read(1)
94
95                 if not in_byte:
96                         break
97
98                 if in_byte == old[matched_byte_count]:
99                         matched_byte_count += 1
100                         if matched_byte_count == len_old:
101                                 modified = True
102                                 matched_byte_count = 0
103                                 end_position = f.tell()
104                                 start_position = end_position - len_old
105                                 if not is_text_file:
106                                         # search backwards for leading slashes written by
107                                         # a previous invocation of this tool
108                                         num_to_write = len_old
109                                         f.seek(start_position - 1)
110                                         while True:
111                                                 if f.read(1) != b'/':
112                                                         break
113                                                 num_to_write += 1
114                                                 f.seek(f.tell() - 2)
115
116                                         # pad with as many leading slashes as necessary
117                                         while num_to_write > len_new:
118                                                 f.write(b'/')
119                                                 num_to_write -= 1
120                                         f.write(new)
121                                 else:
122                                         remainder = f.read()
123                                         f.seek(start_position)
124                                         f.write(new)
125                                         if remainder:
126                                                 f.write(remainder)
127                                                 f.truncate()
128                                                 f.seek(start_position + len_new)
129                 elif matched_byte_count > 0:
130                         # back up an try to start a new match after
131                         # the first byte of the previous partial match
132                         f.seek(f.tell() - matched_byte_count)
133                         matched_byte_count = 0
134
135         f.close()
136         if modified:
137                 if sys.hexversion >= 0x3030000:
138                         orig_mtime = orig_stat.st_mtime_ns
139                         os.utime(filename, ns=(orig_mtime, orig_mtime))
140                 else:
141                         orig_mtime = orig_stat[stat.ST_MTIME]
142                         os.utime(filename, (orig_mtime, orig_mtime))
143         return modified
144
145 def chpath_inplace_symlink(filename, st, old, new):
146         target = os.readlink(filename)
147         if target.startswith(old):
148                 new_target = new + target[len(old):]
149                 os.unlink(filename)
150                 os.symlink(new_target, filename)
151                 os.lchown(filename, st.st_uid, st.st_gid)
152
153 def main(argv):
154
155         parser = ArgumentParser(description=__doc__)
156         try:
157                 parser.add_argument('location', default=None,
158                         help='root directory (e.g. $D)')
159                 parser.add_argument('old', default=None,
160                         help='original build prefix (e.g. /)')
161                 parser.add_argument('new', default=None,
162                         help='new install prefix (e.g. $EPREFIX)')
163                 opts = parser.parse_args(argv)
164
165                 location, old, new = opts.location, opts.old, opts.new
166         except OptionError:
167                 # Argument parsing compatibility for Python 2.6 using optparse.
168                 if sys.hexversion < 0x2070000:
169                         parser = OptionParser(description=__doc__,
170                                 usage="usage: %prog [-h] location old new\n\n" + \
171                                 "  location: root directory (e.g. $D)\n" + \
172                                 "  old:      original build prefix (e.g. /)\n" + \
173                                 "  new:      new install prefix (e.g. $EPREFIX)")
174
175                         (opts, args) = parser.parse_args()
176
177                         if len(args) != 3:
178                                 parser.print_usage()
179                                 print("%s: error: expected 3 arguments, got %i"
180                                         % (__file__, len(args)))
181                                 return
182
183                         location, old, new = args[0:3]
184                 else:
185                         raise
186
187         is_text_file = IsTextFile()
188
189         if not isinstance(location, bytes):
190                 location = location.encode(FS_ENCODING)
191         if not isinstance(old, bytes):
192                 old = old.encode(FS_ENCODING)
193         if not isinstance(new, bytes):
194                 new = new.encode(FS_ENCODING)
195
196         st = os.lstat(location)
197
198         if stat.S_ISDIR(st.st_mode):
199                 for parent, dirs, files in os.walk(location):
200                         for filename in files:
201                                 filename = os.path.join(parent, filename)
202                                 try:
203                                         st = os.lstat(filename)
204                                 except OSError:
205                                         pass
206                                 else:
207                                         if stat.S_ISREG(st.st_mode):
208                                                 chpath_inplace(filename,
209                                                         is_text_file(filename), old, new)
210                                         elif stat.S_ISLNK(st.st_mode):
211                                                 chpath_inplace_symlink(filename, st, old, new)
212
213         elif stat.S_ISREG(st.st_mode):
214                 chpath_inplace(location,
215                         is_text_file(location), old, new)
216         elif stat.S_ISLNK(st.st_mode):
217                 chpath_inplace_symlink(location, st, old, new)
218
219         return os.EX_OK
220
221 if __name__ == '__main__':
222         sys.exit(main(sys.argv[1:]))