python/mozbuild/mozpack/files.py

Fri, 16 Jan 2015 18:13:44 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Fri, 16 Jan 2015 18:13:44 +0100
branch
TOR_BUG_9701
changeset 14
925c144e1f1f
permissions
-rw-r--r--

Integrate suggestion from review to improve consistency with existing code.

michael@0 1 # This Source Code Form is subject to the terms of the Mozilla Public
michael@0 2 # License, v. 2.0. If a copy of the MPL was not distributed with this
michael@0 3 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
michael@0 4
michael@0 5 import errno
michael@0 6 import os
michael@0 7 import platform
michael@0 8 import shutil
michael@0 9 import stat
michael@0 10 import subprocess
michael@0 11 import uuid
michael@0 12 import mozbuild.makeutil as makeutil
michael@0 13 from mozbuild.preprocessor import Preprocessor
michael@0 14 from mozbuild.util import FileAvoidWrite
michael@0 15 from mozpack.executables import (
michael@0 16 is_executable,
michael@0 17 may_strip,
michael@0 18 strip,
michael@0 19 may_elfhack,
michael@0 20 elfhack,
michael@0 21 )
michael@0 22 from mozpack.chrome.manifest import ManifestEntry
michael@0 23 from io import BytesIO
michael@0 24 from mozpack.errors import (
michael@0 25 ErrorMessage,
michael@0 26 errors,
michael@0 27 )
michael@0 28 from mozpack.mozjar import JarReader
michael@0 29 import mozpack.path
michael@0 30 from collections import OrderedDict
michael@0 31 from jsmin import JavascriptMinify
michael@0 32 from tempfile import (
michael@0 33 mkstemp,
michael@0 34 NamedTemporaryFile,
michael@0 35 )
michael@0 36
michael@0 37
michael@0 38 class Dest(object):
michael@0 39 '''
michael@0 40 Helper interface for BaseFile.copy. The interface works as follows:
michael@0 41 - read() and write() can be used to sequentially read/write from the
michael@0 42 underlying file.
michael@0 43 - a call to read() after a write() will re-open the underlying file and
michael@0 44 read from it.
michael@0 45 - a call to write() after a read() will re-open the underlying file,
michael@0 46 emptying it, and write to it.
michael@0 47 '''
michael@0 48 def __init__(self, path):
michael@0 49 self.path = path
michael@0 50 self.mode = None
michael@0 51
michael@0 52 @property
michael@0 53 def name(self):
michael@0 54 return self.path
michael@0 55
michael@0 56 def read(self, length=-1):
michael@0 57 if self.mode != 'r':
michael@0 58 self.file = open(self.path, 'rb')
michael@0 59 self.mode = 'r'
michael@0 60 return self.file.read(length)
michael@0 61
michael@0 62 def write(self, data):
michael@0 63 if self.mode != 'w':
michael@0 64 self.file = open(self.path, 'wb')
michael@0 65 self.mode = 'w'
michael@0 66 return self.file.write(data)
michael@0 67
michael@0 68 def exists(self):
michael@0 69 return os.path.exists(self.path)
michael@0 70
michael@0 71 def close(self):
michael@0 72 if self.mode:
michael@0 73 self.mode = None
michael@0 74 self.file.close()
michael@0 75
michael@0 76
michael@0 77 class BaseFile(object):
michael@0 78 '''
michael@0 79 Base interface and helper for file copying. Derived class may implement
michael@0 80 their own copy function, or rely on BaseFile.copy using the open() member
michael@0 81 function and/or the path property.
michael@0 82 '''
michael@0 83 @staticmethod
michael@0 84 def is_older(first, second):
michael@0 85 '''
michael@0 86 Compares the modification time of two files, and returns whether the
michael@0 87 ``first`` file is older than the ``second`` file.
michael@0 88 '''
michael@0 89 # os.path.getmtime returns a result in seconds with precision up to
michael@0 90 # the microsecond. But microsecond is too precise because
michael@0 91 # shutil.copystat only copies milliseconds, and seconds is not
michael@0 92 # enough precision.
michael@0 93 return int(os.path.getmtime(first) * 1000) \
michael@0 94 <= int(os.path.getmtime(second) * 1000)
michael@0 95
michael@0 96 @staticmethod
michael@0 97 def any_newer(dest, inputs):
michael@0 98 '''
michael@0 99 Compares the modification time of ``dest`` to multiple input files, and
michael@0 100 returns whether any of the ``inputs`` is newer (has a later mtime) than
michael@0 101 ``dest``.
michael@0 102 '''
michael@0 103 # os.path.getmtime returns a result in seconds with precision up to
michael@0 104 # the microsecond. But microsecond is too precise because
michael@0 105 # shutil.copystat only copies milliseconds, and seconds is not
michael@0 106 # enough precision.
michael@0 107 dest_mtime = int(os.path.getmtime(dest) * 1000)
michael@0 108 for input in inputs:
michael@0 109 if dest_mtime < int(os.path.getmtime(input) * 1000):
michael@0 110 return True
michael@0 111 return False
michael@0 112
michael@0 113 def copy(self, dest, skip_if_older=True):
michael@0 114 '''
michael@0 115 Copy the BaseFile content to the destination given as a string or a
michael@0 116 Dest instance. Avoids replacing existing files if the BaseFile content
michael@0 117 matches that of the destination, or in case of plain files, if the
michael@0 118 destination is newer than the original file. This latter behaviour is
michael@0 119 disabled when skip_if_older is False.
michael@0 120 Returns whether a copy was actually performed (True) or not (False).
michael@0 121 '''
michael@0 122 if isinstance(dest, basestring):
michael@0 123 dest = Dest(dest)
michael@0 124 else:
michael@0 125 assert isinstance(dest, Dest)
michael@0 126
michael@0 127 can_skip_content_check = False
michael@0 128 if not dest.exists():
michael@0 129 can_skip_content_check = True
michael@0 130 elif getattr(self, 'path', None) and getattr(dest, 'path', None):
michael@0 131 if skip_if_older and BaseFile.is_older(self.path, dest.path):
michael@0 132 return False
michael@0 133 elif os.path.getsize(self.path) != os.path.getsize(dest.path):
michael@0 134 can_skip_content_check = True
michael@0 135
michael@0 136 if can_skip_content_check:
michael@0 137 if getattr(self, 'path', None) and getattr(dest, 'path', None):
michael@0 138 shutil.copy2(self.path, dest.path)
michael@0 139 else:
michael@0 140 # Ensure the file is always created
michael@0 141 if not dest.exists():
michael@0 142 dest.write('')
michael@0 143 shutil.copyfileobj(self.open(), dest)
michael@0 144 return True
michael@0 145
michael@0 146 src = self.open()
michael@0 147 copy_content = ''
michael@0 148 while True:
michael@0 149 dest_content = dest.read(32768)
michael@0 150 src_content = src.read(32768)
michael@0 151 copy_content += src_content
michael@0 152 if len(dest_content) == len(src_content) == 0:
michael@0 153 break
michael@0 154 # If the read content differs between origin and destination,
michael@0 155 # write what was read up to now, and copy the remainder.
michael@0 156 if dest_content != src_content:
michael@0 157 dest.write(copy_content)
michael@0 158 shutil.copyfileobj(src, dest)
michael@0 159 break
michael@0 160 if hasattr(self, 'path') and hasattr(dest, 'path'):
michael@0 161 shutil.copystat(self.path, dest.path)
michael@0 162 return True
michael@0 163
michael@0 164 def open(self):
michael@0 165 '''
michael@0 166 Return a file-like object allowing to read() the content of the
michael@0 167 associated file. This is meant to be overloaded in subclasses to return
michael@0 168 a custom file-like object.
michael@0 169 '''
michael@0 170 assert self.path is not None
michael@0 171 return open(self.path, 'rb')
michael@0 172
michael@0 173 @property
michael@0 174 def mode(self):
michael@0 175 '''
michael@0 176 Return the file's unix mode, or None if it has no meaning.
michael@0 177 '''
michael@0 178 return None
michael@0 179
michael@0 180
michael@0 181 class File(BaseFile):
michael@0 182 '''
michael@0 183 File class for plain files.
michael@0 184 '''
michael@0 185 def __init__(self, path):
michael@0 186 self.path = path
michael@0 187
michael@0 188 @property
michael@0 189 def mode(self):
michael@0 190 '''
michael@0 191 Return the file's unix mode, as returned by os.stat().st_mode.
michael@0 192 '''
michael@0 193 if platform.system() == 'Windows':
michael@0 194 return None
michael@0 195 assert self.path is not None
michael@0 196 return os.stat(self.path).st_mode
michael@0 197
michael@0 198 class ExecutableFile(File):
michael@0 199 '''
michael@0 200 File class for executable and library files on OS/2, OS/X and ELF systems.
michael@0 201 (see mozpack.executables.is_executable documentation).
michael@0 202 '''
michael@0 203 def copy(self, dest, skip_if_older=True):
michael@0 204 real_dest = dest
michael@0 205 if not isinstance(dest, basestring):
michael@0 206 fd, dest = mkstemp()
michael@0 207 os.close(fd)
michael@0 208 os.remove(dest)
michael@0 209 assert isinstance(dest, basestring)
michael@0 210 # If File.copy didn't actually copy because dest is newer, check the
michael@0 211 # file sizes. If dest is smaller, it means it is already stripped and
michael@0 212 # elfhacked, so we can skip.
michael@0 213 if not File.copy(self, dest, skip_if_older) and \
michael@0 214 os.path.getsize(self.path) > os.path.getsize(dest):
michael@0 215 return False
michael@0 216 try:
michael@0 217 if may_strip(dest):
michael@0 218 strip(dest)
michael@0 219 if may_elfhack(dest):
michael@0 220 elfhack(dest)
michael@0 221 except ErrorMessage:
michael@0 222 os.remove(dest)
michael@0 223 raise
michael@0 224
michael@0 225 if real_dest != dest:
michael@0 226 f = File(dest)
michael@0 227 ret = f.copy(real_dest, skip_if_older)
michael@0 228 os.remove(dest)
michael@0 229 return ret
michael@0 230 return True
michael@0 231
michael@0 232
michael@0 233 class AbsoluteSymlinkFile(File):
michael@0 234 '''File class that is copied by symlinking (if available).
michael@0 235
michael@0 236 This class only works if the target path is absolute.
michael@0 237 '''
michael@0 238
michael@0 239 def __init__(self, path):
michael@0 240 if not os.path.isabs(path):
michael@0 241 raise ValueError('Symlink target not absolute: %s' % path)
michael@0 242
michael@0 243 File.__init__(self, path)
michael@0 244
michael@0 245 def copy(self, dest, skip_if_older=True):
michael@0 246 assert isinstance(dest, basestring)
michael@0 247
michael@0 248 # The logic in this function is complicated by the fact that symlinks
michael@0 249 # aren't universally supported. So, where symlinks aren't supported, we
michael@0 250 # fall back to file copying. Keep in mind that symlink support is
michael@0 251 # per-filesystem, not per-OS.
michael@0 252
michael@0 253 # Handle the simple case where symlinks are definitely not supported by
michael@0 254 # falling back to file copy.
michael@0 255 if not hasattr(os, 'symlink'):
michael@0 256 return File.copy(self, dest, skip_if_older=skip_if_older)
michael@0 257
michael@0 258 # Always verify the symlink target path exists.
michael@0 259 if not os.path.exists(self.path):
michael@0 260 raise ErrorMessage('Symlink target path does not exist: %s' % self.path)
michael@0 261
michael@0 262 st = None
michael@0 263
michael@0 264 try:
michael@0 265 st = os.lstat(dest)
michael@0 266 except OSError as ose:
michael@0 267 if ose.errno != errno.ENOENT:
michael@0 268 raise
michael@0 269
michael@0 270 # If the dest is a symlink pointing to us, we have nothing to do.
michael@0 271 # If it's the wrong symlink, the filesystem must support symlinks,
michael@0 272 # so we replace with a proper symlink.
michael@0 273 if st and stat.S_ISLNK(st.st_mode):
michael@0 274 link = os.readlink(dest)
michael@0 275 if link == self.path:
michael@0 276 return False
michael@0 277
michael@0 278 os.remove(dest)
michael@0 279 os.symlink(self.path, dest)
michael@0 280 return True
michael@0 281
michael@0 282 # If the destination doesn't exist, we try to create a symlink. If that
michael@0 283 # fails, we fall back to copy code.
michael@0 284 if not st:
michael@0 285 try:
michael@0 286 os.symlink(self.path, dest)
michael@0 287 return True
michael@0 288 except OSError:
michael@0 289 return File.copy(self, dest, skip_if_older=skip_if_older)
michael@0 290
michael@0 291 # Now the complicated part. If the destination exists, we could be
michael@0 292 # replacing a file with a symlink. Or, the filesystem may not support
michael@0 293 # symlinks. We want to minimize I/O overhead for performance reasons,
michael@0 294 # so we keep the existing destination file around as long as possible.
michael@0 295 # A lot of the system calls would be eliminated if we cached whether
michael@0 296 # symlinks are supported. However, even if we performed a single
michael@0 297 # up-front test of whether the root of the destination directory
michael@0 298 # supports symlinks, there's no guarantee that all operations for that
michael@0 299 # dest (or source) would be on the same filesystem and would support
michael@0 300 # symlinks.
michael@0 301 #
michael@0 302 # Our strategy is to attempt to create a new symlink with a random
michael@0 303 # name. If that fails, we fall back to copy mode. If that works, we
michael@0 304 # remove the old destination and move the newly-created symlink into
michael@0 305 # its place.
michael@0 306
michael@0 307 temp_dest = os.path.join(os.path.dirname(dest), str(uuid.uuid4()))
michael@0 308 try:
michael@0 309 os.symlink(self.path, temp_dest)
michael@0 310 # TODO Figure out exactly how symlink creation fails and only trap
michael@0 311 # that.
michael@0 312 except EnvironmentError:
michael@0 313 return File.copy(self, dest, skip_if_older=skip_if_older)
michael@0 314
michael@0 315 # If removing the original file fails, don't forget to clean up the
michael@0 316 # temporary symlink.
michael@0 317 try:
michael@0 318 os.remove(dest)
michael@0 319 except EnvironmentError:
michael@0 320 os.remove(temp_dest)
michael@0 321 raise
michael@0 322
michael@0 323 os.rename(temp_dest, dest)
michael@0 324 return True
michael@0 325
michael@0 326
michael@0 327 class ExistingFile(BaseFile):
michael@0 328 '''
michael@0 329 File class that represents a file that may exist but whose content comes
michael@0 330 from elsewhere.
michael@0 331
michael@0 332 This purpose of this class is to account for files that are installed via
michael@0 333 external means. It is typically only used in manifests or in registries to
michael@0 334 account for files.
michael@0 335
michael@0 336 When asked to copy, this class does nothing because nothing is known about
michael@0 337 the source file/data.
michael@0 338
michael@0 339 Instances of this class come in two flavors: required and optional. If an
michael@0 340 existing file is required, it must exist during copy() or an error is
michael@0 341 raised.
michael@0 342 '''
michael@0 343 def __init__(self, required):
michael@0 344 self.required = required
michael@0 345
michael@0 346 def copy(self, dest, skip_if_older=True):
michael@0 347 if isinstance(dest, basestring):
michael@0 348 dest = Dest(dest)
michael@0 349 else:
michael@0 350 assert isinstance(dest, Dest)
michael@0 351
michael@0 352 if not self.required:
michael@0 353 return
michael@0 354
michael@0 355 if not dest.exists():
michael@0 356 errors.fatal("Required existing file doesn't exist: %s" %
michael@0 357 dest.path)
michael@0 358
michael@0 359
michael@0 360 class PreprocessedFile(BaseFile):
michael@0 361 '''
michael@0 362 File class for a file that is preprocessed. PreprocessedFile.copy() runs
michael@0 363 the preprocessor on the file to create the output.
michael@0 364 '''
michael@0 365 def __init__(self, path, depfile_path, marker, defines, extra_depends=None):
michael@0 366 self.path = path
michael@0 367 self.depfile = depfile_path
michael@0 368 self.marker = marker
michael@0 369 self.defines = defines
michael@0 370 self.extra_depends = list(extra_depends or [])
michael@0 371
michael@0 372 def copy(self, dest, skip_if_older=True):
michael@0 373 '''
michael@0 374 Invokes the preprocessor to create the destination file.
michael@0 375 '''
michael@0 376 if isinstance(dest, basestring):
michael@0 377 dest = Dest(dest)
michael@0 378 else:
michael@0 379 assert isinstance(dest, Dest)
michael@0 380
michael@0 381 # We have to account for the case where the destination exists and is a
michael@0 382 # symlink to something. Since we know the preprocessor is certainly not
michael@0 383 # going to create a symlink, we can just remove the existing one. If the
michael@0 384 # destination is not a symlink, we leave it alone, since we're going to
michael@0 385 # overwrite its contents anyway.
michael@0 386 # If symlinks aren't supported at all, we can skip this step.
michael@0 387 if hasattr(os, 'symlink'):
michael@0 388 if os.path.islink(dest.path):
michael@0 389 os.remove(dest.path)
michael@0 390
michael@0 391 pp_deps = set(self.extra_depends)
michael@0 392
michael@0 393 # If a dependency file was specified, and it exists, add any
michael@0 394 # dependencies from that file to our list.
michael@0 395 if self.depfile and os.path.exists(self.depfile):
michael@0 396 target = mozpack.path.normpath(dest.name)
michael@0 397 with open(self.depfile, 'rb') as fileobj:
michael@0 398 for rule in makeutil.read_dep_makefile(fileobj):
michael@0 399 if target in rule.targets():
michael@0 400 pp_deps.update(rule.dependencies())
michael@0 401
michael@0 402 skip = False
michael@0 403 if dest.exists() and skip_if_older:
michael@0 404 # If a dependency file was specified, and it doesn't exist,
michael@0 405 # assume that the preprocessor needs to be rerun. That will
michael@0 406 # regenerate the dependency file.
michael@0 407 if self.depfile and not os.path.exists(self.depfile):
michael@0 408 skip = False
michael@0 409 else:
michael@0 410 skip = not BaseFile.any_newer(dest.path, pp_deps)
michael@0 411
michael@0 412 if skip:
michael@0 413 return False
michael@0 414
michael@0 415 deps_out = None
michael@0 416 if self.depfile:
michael@0 417 deps_out = FileAvoidWrite(self.depfile)
michael@0 418 pp = Preprocessor(defines=self.defines, marker=self.marker)
michael@0 419
michael@0 420 with open(self.path, 'rU') as input:
michael@0 421 pp.processFile(input=input, output=dest, depfile=deps_out)
michael@0 422
michael@0 423 dest.close()
michael@0 424 if self.depfile:
michael@0 425 deps_out.close()
michael@0 426
michael@0 427 return True
michael@0 428
michael@0 429
michael@0 430 class GeneratedFile(BaseFile):
michael@0 431 '''
michael@0 432 File class for content with no previous existence on the filesystem.
michael@0 433 '''
michael@0 434 def __init__(self, content):
michael@0 435 self.content = content
michael@0 436
michael@0 437 def open(self):
michael@0 438 return BytesIO(self.content)
michael@0 439
michael@0 440
michael@0 441 class DeflatedFile(BaseFile):
michael@0 442 '''
michael@0 443 File class for members of a jar archive. DeflatedFile.copy() effectively
michael@0 444 extracts the file from the jar archive.
michael@0 445 '''
michael@0 446 def __init__(self, file):
michael@0 447 from mozpack.mozjar import JarFileReader
michael@0 448 assert isinstance(file, JarFileReader)
michael@0 449 self.file = file
michael@0 450
michael@0 451 def open(self):
michael@0 452 self.file.seek(0)
michael@0 453 return self.file
michael@0 454
michael@0 455
michael@0 456 class XPTFile(GeneratedFile):
michael@0 457 '''
michael@0 458 File class for a linked XPT file. It takes several XPT files as input
michael@0 459 (using the add() and remove() member functions), and links them at copy()
michael@0 460 time.
michael@0 461 '''
michael@0 462 def __init__(self):
michael@0 463 self._files = set()
michael@0 464
michael@0 465 def add(self, xpt):
michael@0 466 '''
michael@0 467 Add the given XPT file (as a BaseFile instance) to the list of XPTs
michael@0 468 to link.
michael@0 469 '''
michael@0 470 assert isinstance(xpt, BaseFile)
michael@0 471 self._files.add(xpt)
michael@0 472
michael@0 473 def remove(self, xpt):
michael@0 474 '''
michael@0 475 Remove the given XPT file (as a BaseFile instance) from the list of
michael@0 476 XPTs to link.
michael@0 477 '''
michael@0 478 assert isinstance(xpt, BaseFile)
michael@0 479 self._files.remove(xpt)
michael@0 480
michael@0 481 def copy(self, dest, skip_if_older=True):
michael@0 482 '''
michael@0 483 Link the registered XPTs and place the resulting linked XPT at the
michael@0 484 destination given as a string or a Dest instance. Avoids an expensive
michael@0 485 XPT linking if the interfaces in an existing destination match those of
michael@0 486 the individual XPTs to link.
michael@0 487 skip_if_older is ignored.
michael@0 488 '''
michael@0 489 if isinstance(dest, basestring):
michael@0 490 dest = Dest(dest)
michael@0 491 assert isinstance(dest, Dest)
michael@0 492
michael@0 493 from xpt import xpt_link, Typelib, Interface
michael@0 494 all_typelibs = [Typelib.read(f.open()) for f in self._files]
michael@0 495 if dest.exists():
michael@0 496 # Typelib.read() needs to seek(), so use a BytesIO for dest
michael@0 497 # content.
michael@0 498 dest_interfaces = \
michael@0 499 dict((i.name, i)
michael@0 500 for i in Typelib.read(BytesIO(dest.read())).interfaces
michael@0 501 if i.iid != Interface.UNRESOLVED_IID)
michael@0 502 identical = True
michael@0 503 for f in self._files:
michael@0 504 typelib = Typelib.read(f.open())
michael@0 505 for i in typelib.interfaces:
michael@0 506 if i.iid != Interface.UNRESOLVED_IID and \
michael@0 507 not (i.name in dest_interfaces and
michael@0 508 i == dest_interfaces[i.name]):
michael@0 509 identical = False
michael@0 510 break
michael@0 511 if identical:
michael@0 512 return False
michael@0 513 s = BytesIO()
michael@0 514 xpt_link(all_typelibs).write(s)
michael@0 515 dest.write(s.getvalue())
michael@0 516 return True
michael@0 517
michael@0 518 def open(self):
michael@0 519 raise RuntimeError("Unsupported")
michael@0 520
michael@0 521 def isempty(self):
michael@0 522 '''
michael@0 523 Return whether there are XPT files to link.
michael@0 524 '''
michael@0 525 return len(self._files) == 0
michael@0 526
michael@0 527
michael@0 528 class ManifestFile(BaseFile):
michael@0 529 '''
michael@0 530 File class for a manifest file. It takes individual manifest entries (using
michael@0 531 the add() and remove() member functions), and adjusts them to be relative
michael@0 532 to the base path for the manifest, given at creation.
michael@0 533 Example:
michael@0 534 There is a manifest entry "content webapprt webapprt/content/" relative
michael@0 535 to "webapprt/chrome". When packaging, the entry will be stored in
michael@0 536 jar:webapprt/omni.ja!/chrome/chrome.manifest, which means the entry
michael@0 537 will have to be relative to "chrome" instead of "webapprt/chrome". This
michael@0 538 doesn't really matter when serializing the entry, since this base path
michael@0 539 is not written out, but it matters when moving the entry at the same
michael@0 540 time, e.g. to jar:webapprt/omni.ja!/chrome.manifest, which we don't do
michael@0 541 currently but could in the future.
michael@0 542 '''
michael@0 543 def __init__(self, base, entries=None):
michael@0 544 self._entries = entries if entries else []
michael@0 545 self._base = base
michael@0 546
michael@0 547 def add(self, entry):
michael@0 548 '''
michael@0 549 Add the given entry to the manifest. Entries are rebased at open() time
michael@0 550 instead of add() time so that they can be more easily remove()d.
michael@0 551 '''
michael@0 552 assert isinstance(entry, ManifestEntry)
michael@0 553 self._entries.append(entry)
michael@0 554
michael@0 555 def remove(self, entry):
michael@0 556 '''
michael@0 557 Remove the given entry from the manifest.
michael@0 558 '''
michael@0 559 assert isinstance(entry, ManifestEntry)
michael@0 560 self._entries.remove(entry)
michael@0 561
michael@0 562 def open(self):
michael@0 563 '''
michael@0 564 Return a file-like object allowing to read() the serialized content of
michael@0 565 the manifest.
michael@0 566 '''
michael@0 567 return BytesIO(''.join('%s\n' % e.rebase(self._base)
michael@0 568 for e in self._entries))
michael@0 569
michael@0 570 def __iter__(self):
michael@0 571 '''
michael@0 572 Iterate over entries in the manifest file.
michael@0 573 '''
michael@0 574 return iter(self._entries)
michael@0 575
michael@0 576 def isempty(self):
michael@0 577 '''
michael@0 578 Return whether there are manifest entries to write
michael@0 579 '''
michael@0 580 return len(self._entries) == 0
michael@0 581
michael@0 582
michael@0 583 class MinifiedProperties(BaseFile):
michael@0 584 '''
michael@0 585 File class for minified properties. This wraps around a BaseFile instance,
michael@0 586 and removes lines starting with a # from its content.
michael@0 587 '''
michael@0 588 def __init__(self, file):
michael@0 589 assert isinstance(file, BaseFile)
michael@0 590 self._file = file
michael@0 591
michael@0 592 def open(self):
michael@0 593 '''
michael@0 594 Return a file-like object allowing to read() the minified content of
michael@0 595 the properties file.
michael@0 596 '''
michael@0 597 return BytesIO(''.join(l for l in self._file.open().readlines()
michael@0 598 if not l.startswith('#')))
michael@0 599
michael@0 600
michael@0 601 class MinifiedJavaScript(BaseFile):
michael@0 602 '''
michael@0 603 File class for minifying JavaScript files.
michael@0 604 '''
michael@0 605 def __init__(self, file, verify_command=None):
michael@0 606 assert isinstance(file, BaseFile)
michael@0 607 self._file = file
michael@0 608 self._verify_command = verify_command
michael@0 609
michael@0 610 def open(self):
michael@0 611 output = BytesIO()
michael@0 612 minify = JavascriptMinify(self._file.open(), output)
michael@0 613 minify.minify()
michael@0 614 output.seek(0)
michael@0 615
michael@0 616 if not self._verify_command:
michael@0 617 return output
michael@0 618
michael@0 619 input_source = self._file.open().read()
michael@0 620 output_source = output.getvalue()
michael@0 621
michael@0 622 with NamedTemporaryFile() as fh1, NamedTemporaryFile() as fh2:
michael@0 623 fh1.write(input_source)
michael@0 624 fh2.write(output_source)
michael@0 625 fh1.flush()
michael@0 626 fh2.flush()
michael@0 627
michael@0 628 try:
michael@0 629 args = list(self._verify_command)
michael@0 630 args.extend([fh1.name, fh2.name])
michael@0 631 subprocess.check_output(args, stderr=subprocess.STDOUT)
michael@0 632 except subprocess.CalledProcessError as e:
michael@0 633 errors.warn('JS minification verification failed for %s:' %
michael@0 634 (getattr(self._file, 'path', '<unknown>')))
michael@0 635 # Prefix each line with "Warning:" so mozharness doesn't
michael@0 636 # think these error messages are real errors.
michael@0 637 for line in e.output.splitlines():
michael@0 638 errors.warn(line)
michael@0 639
michael@0 640 return self._file.open()
michael@0 641
michael@0 642 return output
michael@0 643
michael@0 644
michael@0 645 class BaseFinder(object):
michael@0 646 def __init__(self, base, minify=False, minify_js=False,
michael@0 647 minify_js_verify_command=None):
michael@0 648 '''
michael@0 649 Initializes the instance with a reference base directory.
michael@0 650
michael@0 651 The optional minify argument specifies whether minification of code
michael@0 652 should occur. minify_js is an additional option to control minification
michael@0 653 of JavaScript. It requires minify to be True.
michael@0 654
michael@0 655 minify_js_verify_command can be used to optionally verify the results
michael@0 656 of JavaScript minification. If defined, it is expected to be an iterable
michael@0 657 that will constitute the first arguments to a called process which will
michael@0 658 receive the filenames of the original and minified JavaScript files.
michael@0 659 The invoked process can then verify the results. If minification is
michael@0 660 rejected, the process exits with a non-0 exit code and the original
michael@0 661 JavaScript source is used. An example value for this argument is
michael@0 662 ('/path/to/js', '/path/to/verify/script.js').
michael@0 663 '''
michael@0 664 if minify_js and not minify:
michael@0 665 raise ValueError('minify_js requires minify.')
michael@0 666
michael@0 667 self.base = base
michael@0 668 self._minify = minify
michael@0 669 self._minify_js = minify_js
michael@0 670 self._minify_js_verify_command = minify_js_verify_command
michael@0 671
michael@0 672 def find(self, pattern):
michael@0 673 '''
michael@0 674 Yield path, BaseFile_instance pairs for all files under the base
michael@0 675 directory and its subdirectories that match the given pattern. See the
michael@0 676 mozpack.path.match documentation for a description of the handled
michael@0 677 patterns.
michael@0 678 '''
michael@0 679 while pattern.startswith('/'):
michael@0 680 pattern = pattern[1:]
michael@0 681 for p, f in self._find(pattern):
michael@0 682 yield p, self._minify_file(p, f)
michael@0 683
michael@0 684 def __iter__(self):
michael@0 685 '''
michael@0 686 Iterates over all files under the base directory (excluding files
michael@0 687 starting with a '.' and files at any level under a directory starting
michael@0 688 with a '.').
michael@0 689 for path, file in finder:
michael@0 690 ...
michael@0 691 '''
michael@0 692 return self.find('')
michael@0 693
michael@0 694 def __contains__(self, pattern):
michael@0 695 raise RuntimeError("'in' operator forbidden for %s. Use contains()." %
michael@0 696 self.__class__.__name__)
michael@0 697
michael@0 698 def contains(self, pattern):
michael@0 699 '''
michael@0 700 Return whether some files under the base directory match the given
michael@0 701 pattern. See the mozpack.path.match documentation for a description of
michael@0 702 the handled patterns.
michael@0 703 '''
michael@0 704 return any(self.find(pattern))
michael@0 705
michael@0 706 def _minify_file(self, path, file):
michael@0 707 '''
michael@0 708 Return an appropriate MinifiedSomething wrapper for the given BaseFile
michael@0 709 instance (file), according to the file type (determined by the given
michael@0 710 path), if the FileFinder was created with minification enabled.
michael@0 711 Otherwise, just return the given BaseFile instance.
michael@0 712 '''
michael@0 713 if not self._minify or isinstance(file, ExecutableFile):
michael@0 714 return file
michael@0 715
michael@0 716 if path.endswith('.properties'):
michael@0 717 return MinifiedProperties(file)
michael@0 718
michael@0 719 if self._minify_js and path.endswith(('.js', '.jsm')):
michael@0 720 return MinifiedJavaScript(file, self._minify_js_verify_command)
michael@0 721
michael@0 722 return file
michael@0 723
michael@0 724
michael@0 725 class FileFinder(BaseFinder):
michael@0 726 '''
michael@0 727 Helper to get appropriate BaseFile instances from the file system.
michael@0 728 '''
michael@0 729 def __init__(self, base, find_executables=True, ignore=(), **kargs):
michael@0 730 '''
michael@0 731 Create a FileFinder for files under the given base directory.
michael@0 732
michael@0 733 The find_executables argument determines whether the finder needs to
michael@0 734 try to guess whether files are executables. Disabling this guessing
michael@0 735 when not necessary can speed up the finder significantly.
michael@0 736
michael@0 737 ``ignore`` accepts an iterable of patterns to ignore. Entries are
michael@0 738 strings that match paths relative to ``base`` using
michael@0 739 ``mozpack.path.match()``. This means if an entry corresponds
michael@0 740 to a directory, all files under that directory will be ignored. If
michael@0 741 an entry corresponds to a file, that particular file will be ignored.
michael@0 742 '''
michael@0 743 BaseFinder.__init__(self, base, **kargs)
michael@0 744 self.find_executables = find_executables
michael@0 745 self.ignore = ignore
michael@0 746
michael@0 747 def _find(self, pattern):
michael@0 748 '''
michael@0 749 Actual implementation of FileFinder.find(), dispatching to specialized
michael@0 750 member functions depending on what kind of pattern was given.
michael@0 751 Note all files with a name starting with a '.' are ignored when
michael@0 752 scanning directories, but are not ignored when explicitely requested.
michael@0 753 '''
michael@0 754 if '*' in pattern:
michael@0 755 return self._find_glob('', mozpack.path.split(pattern))
michael@0 756 elif os.path.isdir(os.path.join(self.base, pattern)):
michael@0 757 return self._find_dir(pattern)
michael@0 758 else:
michael@0 759 return self._find_file(pattern)
michael@0 760
michael@0 761 def _find_dir(self, path):
michael@0 762 '''
michael@0 763 Actual implementation of FileFinder.find() when the given pattern
michael@0 764 corresponds to an existing directory under the base directory.
michael@0 765 Ignores file names starting with a '.' under the given path. If the
michael@0 766 path itself has leafs starting with a '.', they are not ignored.
michael@0 767 '''
michael@0 768 for p in self.ignore:
michael@0 769 if mozpack.path.match(path, p):
michael@0 770 return
michael@0 771
michael@0 772 # The sorted makes the output idempotent. Otherwise, we are
michael@0 773 # likely dependent on filesystem implementation details, such as
michael@0 774 # inode ordering.
michael@0 775 for p in sorted(os.listdir(os.path.join(self.base, path))):
michael@0 776 if p.startswith('.'):
michael@0 777 continue
michael@0 778 for p_, f in self._find(mozpack.path.join(path, p)):
michael@0 779 yield p_, f
michael@0 780
michael@0 781 def _find_file(self, path):
michael@0 782 '''
michael@0 783 Actual implementation of FileFinder.find() when the given pattern
michael@0 784 corresponds to an existing file under the base directory.
michael@0 785 '''
michael@0 786 srcpath = os.path.join(self.base, path)
michael@0 787 if not os.path.exists(srcpath):
michael@0 788 return
michael@0 789
michael@0 790 for p in self.ignore:
michael@0 791 if mozpack.path.match(path, p):
michael@0 792 return
michael@0 793
michael@0 794 if self.find_executables and is_executable(srcpath):
michael@0 795 yield path, ExecutableFile(srcpath)
michael@0 796 else:
michael@0 797 yield path, File(srcpath)
michael@0 798
michael@0 799 def _find_glob(self, base, pattern):
michael@0 800 '''
michael@0 801 Actual implementation of FileFinder.find() when the given pattern
michael@0 802 contains globbing patterns ('*' or '**'). This is meant to be an
michael@0 803 equivalent of:
michael@0 804 for p, f in self:
michael@0 805 if mozpack.path.match(p, pattern):
michael@0 806 yield p, f
michael@0 807 but avoids scanning the entire tree.
michael@0 808 '''
michael@0 809 if not pattern:
michael@0 810 for p, f in self._find(base):
michael@0 811 yield p, f
michael@0 812 elif pattern[0] == '**':
michael@0 813 for p, f in self._find(base):
michael@0 814 if mozpack.path.match(p, mozpack.path.join(*pattern)):
michael@0 815 yield p, f
michael@0 816 elif '*' in pattern[0]:
michael@0 817 if not os.path.exists(os.path.join(self.base, base)):
michael@0 818 return
michael@0 819
michael@0 820 for p in self.ignore:
michael@0 821 if mozpack.path.match(base, p):
michael@0 822 return
michael@0 823
michael@0 824 # See above comment w.r.t. sorted() and idempotent behavior.
michael@0 825 for p in sorted(os.listdir(os.path.join(self.base, base))):
michael@0 826 if p.startswith('.') and not pattern[0].startswith('.'):
michael@0 827 continue
michael@0 828 if mozpack.path.match(p, pattern[0]):
michael@0 829 for p_, f in self._find_glob(mozpack.path.join(base, p),
michael@0 830 pattern[1:]):
michael@0 831 yield p_, f
michael@0 832 else:
michael@0 833 for p, f in self._find_glob(mozpack.path.join(base, pattern[0]),
michael@0 834 pattern[1:]):
michael@0 835 yield p, f
michael@0 836
michael@0 837
michael@0 838 class JarFinder(BaseFinder):
michael@0 839 '''
michael@0 840 Helper to get appropriate DeflatedFile instances from a JarReader.
michael@0 841 '''
michael@0 842 def __init__(self, base, reader, **kargs):
michael@0 843 '''
michael@0 844 Create a JarFinder for files in the given JarReader. The base argument
michael@0 845 is used as an indication of the Jar file location.
michael@0 846 '''
michael@0 847 assert isinstance(reader, JarReader)
michael@0 848 BaseFinder.__init__(self, base, **kargs)
michael@0 849 self._files = OrderedDict((f.filename, f) for f in reader)
michael@0 850
michael@0 851 def _find(self, pattern):
michael@0 852 '''
michael@0 853 Actual implementation of JarFinder.find(), dispatching to specialized
michael@0 854 member functions depending on what kind of pattern was given.
michael@0 855 '''
michael@0 856 if '*' in pattern:
michael@0 857 for p in self._files:
michael@0 858 if mozpack.path.match(p, pattern):
michael@0 859 yield p, DeflatedFile(self._files[p])
michael@0 860 elif pattern == '':
michael@0 861 for p in self._files:
michael@0 862 yield p, DeflatedFile(self._files[p])
michael@0 863 elif pattern in self._files:
michael@0 864 yield pattern, DeflatedFile(self._files[pattern])
michael@0 865 else:
michael@0 866 for p in self._files:
michael@0 867 if mozpack.path.basedir(p, [pattern]) == pattern:
michael@0 868 yield p, DeflatedFile(self._files[p])

mercurial