|
1 # This Source Code Form is subject to the terms of the Mozilla Public |
|
2 # License, v. 2.0. If a copy of the MPL was not distributed with this |
|
3 # file, You can obtain one at http://mozilla.org/MPL/2.0/. |
|
4 |
|
5 from __future__ import unicode_literals |
|
6 |
|
7 from contextlib import contextmanager |
|
8 import json |
|
9 |
|
10 from .files import ( |
|
11 AbsoluteSymlinkFile, |
|
12 ExistingFile, |
|
13 File, |
|
14 FileFinder, |
|
15 PreprocessedFile, |
|
16 ) |
|
17 import mozpack.path as mozpath |
|
18 |
|
19 |
|
20 # This probably belongs in a more generic module. Where? |
|
21 @contextmanager |
|
22 def _auto_fileobj(path, fileobj, mode='r'): |
|
23 if path and fileobj: |
|
24 raise AssertionError('Only 1 of path or fileobj may be defined.') |
|
25 |
|
26 if not path and not fileobj: |
|
27 raise AssertionError('Must specified 1 of path or fileobj.') |
|
28 |
|
29 if path: |
|
30 fileobj = open(path, mode) |
|
31 |
|
32 try: |
|
33 yield fileobj |
|
34 finally: |
|
35 if path: |
|
36 fileobj.close() |
|
37 |
|
38 |
|
39 class UnreadableInstallManifest(Exception): |
|
40 """Raised when an invalid install manifest is parsed.""" |
|
41 |
|
42 |
|
43 class InstallManifest(object): |
|
44 """Describes actions to be used with a copier.FileCopier instance. |
|
45 |
|
46 This class facilitates serialization and deserialization of data used to |
|
47 construct a copier.FileCopier and to perform copy operations. |
|
48 |
|
49 The manifest defines source paths, destination paths, and a mechanism by |
|
50 which the destination file should come into existence. |
|
51 |
|
52 Entries in the manifest correspond to the following types: |
|
53 |
|
54 copy -- The file specified as the source path will be copied to the |
|
55 destination path. |
|
56 |
|
57 symlink -- The destination path will be a symlink to the source path. |
|
58 If symlinks are not supported, a copy will be performed. |
|
59 |
|
60 exists -- The destination path is accounted for and won't be deleted by |
|
61 the FileCopier. If the destination path doesn't exist, an error is |
|
62 raised. |
|
63 |
|
64 optional -- The destination path is accounted for and won't be deleted by |
|
65 the FileCopier. No error is raised if the destination path does not |
|
66 exist. |
|
67 |
|
68 patternsymlink -- Paths matched by the expression in the source path |
|
69 will be symlinked to the destination directory. |
|
70 |
|
71 patterncopy -- Similar to patternsymlink except files are copied, not |
|
72 symlinked. |
|
73 |
|
74 preprocess -- The file specified at the source path will be run through |
|
75 the preprocessor, and the output will be written to the destination |
|
76 path. |
|
77 |
|
78 Version 1 of the manifest was the initial version. |
|
79 Version 2 added optional path support |
|
80 Version 3 added support for pattern entries. |
|
81 Version 4 added preprocessed file support. |
|
82 """ |
|
83 |
|
84 CURRENT_VERSION = 4 |
|
85 |
|
86 FIELD_SEPARATOR = '\x1f' |
|
87 |
|
88 SYMLINK = 1 |
|
89 COPY = 2 |
|
90 REQUIRED_EXISTS = 3 |
|
91 OPTIONAL_EXISTS = 4 |
|
92 PATTERN_SYMLINK = 5 |
|
93 PATTERN_COPY = 6 |
|
94 PREPROCESS = 7 |
|
95 |
|
96 def __init__(self, path=None, fileobj=None): |
|
97 """Create a new InstallManifest entry. |
|
98 |
|
99 If path is defined, the manifest will be populated with data from the |
|
100 file path. |
|
101 |
|
102 If fileobj is defined, the manifest will be populated with data read |
|
103 from the specified file object. |
|
104 |
|
105 Both path and fileobj cannot be defined. |
|
106 """ |
|
107 self._dests = {} |
|
108 self._source_file = None |
|
109 |
|
110 if path or fileobj: |
|
111 with _auto_fileobj(path, fileobj, 'rb') as fh: |
|
112 self._source_file = fh.name |
|
113 self._load_from_fileobj(fh) |
|
114 |
|
115 def _load_from_fileobj(self, fileobj): |
|
116 version = fileobj.readline().rstrip() |
|
117 if version not in ('1', '2', '3', '4'): |
|
118 raise UnreadableInstallManifest('Unknown manifest version: ' % |
|
119 version) |
|
120 |
|
121 for line in fileobj: |
|
122 line = line.rstrip() |
|
123 |
|
124 fields = line.split(self.FIELD_SEPARATOR) |
|
125 |
|
126 record_type = int(fields[0]) |
|
127 |
|
128 if record_type == self.SYMLINK: |
|
129 dest, source = fields[1:] |
|
130 self.add_symlink(source, dest) |
|
131 continue |
|
132 |
|
133 if record_type == self.COPY: |
|
134 dest, source = fields[1:] |
|
135 self.add_copy(source, dest) |
|
136 continue |
|
137 |
|
138 if record_type == self.REQUIRED_EXISTS: |
|
139 _, path = fields |
|
140 self.add_required_exists(path) |
|
141 continue |
|
142 |
|
143 if record_type == self.OPTIONAL_EXISTS: |
|
144 _, path = fields |
|
145 self.add_optional_exists(path) |
|
146 continue |
|
147 |
|
148 if record_type == self.PATTERN_SYMLINK: |
|
149 _, base, pattern, dest = fields[1:] |
|
150 self.add_pattern_symlink(base, pattern, dest) |
|
151 continue |
|
152 |
|
153 if record_type == self.PATTERN_COPY: |
|
154 _, base, pattern, dest = fields[1:] |
|
155 self.add_pattern_copy(base, pattern, dest) |
|
156 continue |
|
157 |
|
158 if record_type == self.PREPROCESS: |
|
159 dest, source, deps, marker, defines = fields[1:] |
|
160 self.add_preprocess(source, dest, deps, marker, |
|
161 self._decode_field_entry(defines)) |
|
162 continue |
|
163 |
|
164 raise UnreadableInstallManifest('Unknown record type: %d' % |
|
165 record_type) |
|
166 |
|
167 def __len__(self): |
|
168 return len(self._dests) |
|
169 |
|
170 def __contains__(self, item): |
|
171 return item in self._dests |
|
172 |
|
173 def __eq__(self, other): |
|
174 return isinstance(other, InstallManifest) and self._dests == other._dests |
|
175 |
|
176 def __neq__(self, other): |
|
177 return not self.__eq__(other) |
|
178 |
|
179 def __ior__(self, other): |
|
180 if not isinstance(other, InstallManifest): |
|
181 raise ValueError('Can only | with another instance of InstallManifest.') |
|
182 |
|
183 for dest in sorted(other._dests): |
|
184 self._add_entry(dest, other._dests[dest]) |
|
185 |
|
186 return self |
|
187 |
|
188 def _encode_field_entry(self, data): |
|
189 """Converts an object into a format that can be stored in the manifest file. |
|
190 |
|
191 Complex data types, such as ``dict``, need to be converted into a text |
|
192 representation before they can be written to a file. |
|
193 """ |
|
194 return json.dumps(data, sort_keys=True) |
|
195 |
|
196 def _decode_field_entry(self, data): |
|
197 """Restores an object from a format that can be stored in the manifest file. |
|
198 |
|
199 Complex data types, such as ``dict``, need to be converted into a text |
|
200 representation before they can be written to a file. |
|
201 """ |
|
202 return json.loads(data) |
|
203 |
|
204 def write(self, path=None, fileobj=None): |
|
205 """Serialize this manifest to a file or file object. |
|
206 |
|
207 If path is specified, that file will be written to. If fileobj is specified, |
|
208 the serialized content will be written to that file object. |
|
209 |
|
210 It is an error if both are specified. |
|
211 """ |
|
212 with _auto_fileobj(path, fileobj, 'wb') as fh: |
|
213 fh.write('%d\n' % self.CURRENT_VERSION) |
|
214 |
|
215 for dest in sorted(self._dests): |
|
216 entry = self._dests[dest] |
|
217 |
|
218 parts = ['%d' % entry[0], dest] |
|
219 parts.extend(entry[1:]) |
|
220 fh.write('%s\n' % self.FIELD_SEPARATOR.join( |
|
221 p.encode('utf-8') for p in parts)) |
|
222 |
|
223 def add_symlink(self, source, dest): |
|
224 """Add a symlink to this manifest. |
|
225 |
|
226 dest will be a symlink to source. |
|
227 """ |
|
228 self._add_entry(dest, (self.SYMLINK, source)) |
|
229 |
|
230 def add_copy(self, source, dest): |
|
231 """Add a copy to this manifest. |
|
232 |
|
233 source will be copied to dest. |
|
234 """ |
|
235 self._add_entry(dest, (self.COPY, source)) |
|
236 |
|
237 def add_required_exists(self, dest): |
|
238 """Record that a destination file must exist. |
|
239 |
|
240 This effectively prevents the listed file from being deleted. |
|
241 """ |
|
242 self._add_entry(dest, (self.REQUIRED_EXISTS,)) |
|
243 |
|
244 def add_optional_exists(self, dest): |
|
245 """Record that a destination file may exist. |
|
246 |
|
247 This effectively prevents the listed file from being deleted. Unlike a |
|
248 "required exists" file, files of this type do not raise errors if the |
|
249 destination file does not exist. |
|
250 """ |
|
251 self._add_entry(dest, (self.OPTIONAL_EXISTS,)) |
|
252 |
|
253 def add_pattern_symlink(self, base, pattern, dest): |
|
254 """Add a pattern match that results in symlinks being created. |
|
255 |
|
256 A ``FileFinder`` will be created with its base set to ``base`` |
|
257 and ``FileFinder.find()`` will be called with ``pattern`` to discover |
|
258 source files. Each source file will be symlinked under ``dest``. |
|
259 |
|
260 Filenames under ``dest`` are constructed by taking the path fragment |
|
261 after ``base`` and concatenating it with ``dest``. e.g. |
|
262 |
|
263 <base>/foo/bar.h -> <dest>/foo/bar.h |
|
264 """ |
|
265 self._add_entry(mozpath.join(base, pattern, dest), |
|
266 (self.PATTERN_SYMLINK, base, pattern, dest)) |
|
267 |
|
268 def add_pattern_copy(self, base, pattern, dest): |
|
269 """Add a pattern match that results in copies. |
|
270 |
|
271 See ``add_pattern_symlink()`` for usage. |
|
272 """ |
|
273 self._add_entry(mozpath.join(base, pattern, dest), |
|
274 (self.PATTERN_COPY, base, pattern, dest)) |
|
275 |
|
276 def add_preprocess(self, source, dest, deps, marker='#', defines={}): |
|
277 """Add a preprocessed file to this manifest. |
|
278 |
|
279 ``source`` will be passed through preprocessor.py, and the output will be |
|
280 written to ``dest``. |
|
281 """ |
|
282 self._add_entry(dest, |
|
283 (self.PREPROCESS, source, deps, marker, self._encode_field_entry(defines))) |
|
284 |
|
285 def _add_entry(self, dest, entry): |
|
286 if dest in self._dests: |
|
287 raise ValueError('Item already in manifest: %s' % dest) |
|
288 |
|
289 self._dests[dest] = entry |
|
290 |
|
291 def _get_deps(self, dest): |
|
292 return {self._source_file} if self._source_file else set() |
|
293 |
|
294 def populate_registry(self, registry): |
|
295 """Populate a mozpack.copier.FileRegistry instance with data from us. |
|
296 |
|
297 The caller supplied a FileRegistry instance (or at least something that |
|
298 conforms to its interface) and that instance is populated with data |
|
299 from this manifest. |
|
300 """ |
|
301 for dest in sorted(self._dests): |
|
302 entry = self._dests[dest] |
|
303 install_type = entry[0] |
|
304 |
|
305 if install_type == self.SYMLINK: |
|
306 registry.add(dest, AbsoluteSymlinkFile(entry[1])) |
|
307 continue |
|
308 |
|
309 if install_type == self.COPY: |
|
310 registry.add(dest, File(entry[1])) |
|
311 continue |
|
312 |
|
313 if install_type == self.REQUIRED_EXISTS: |
|
314 registry.add(dest, ExistingFile(required=True)) |
|
315 continue |
|
316 |
|
317 if install_type == self.OPTIONAL_EXISTS: |
|
318 registry.add(dest, ExistingFile(required=False)) |
|
319 continue |
|
320 |
|
321 if install_type in (self.PATTERN_SYMLINK, self.PATTERN_COPY): |
|
322 _, base, pattern, dest = entry |
|
323 finder = FileFinder(base, find_executables=False) |
|
324 paths = [f[0] for f in finder.find(pattern)] |
|
325 |
|
326 if install_type == self.PATTERN_SYMLINK: |
|
327 cls = AbsoluteSymlinkFile |
|
328 else: |
|
329 cls = File |
|
330 |
|
331 for path in paths: |
|
332 source = mozpath.join(base, path) |
|
333 registry.add(mozpath.join(dest, path), cls(source)) |
|
334 |
|
335 continue |
|
336 |
|
337 if install_type == self.PREPROCESS: |
|
338 registry.add(dest, PreprocessedFile(entry[1], |
|
339 depfile_path=entry[2], |
|
340 marker=entry[3], |
|
341 defines=self._decode_field_entry(entry[4]), |
|
342 extra_depends=self._get_deps(dest))) |
|
343 |
|
344 continue |
|
345 |
|
346 raise Exception('Unknown install type defined in manifest: %d' % |
|
347 install_type) |