updater.py 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193
  1. """The auto-rebuild system is an optional part of webassets that can be used
  2. during development, and can also be quite convenient on small sites that don't
  3. have the performance requirements where a rebuild-check on every request is
  4. fatal.
  5. This module contains classes that help determine whether a rebuild is required
  6. for a bundle. This is more complicated than simply comparing the timestamps of
  7. the source and output files.
  8. First, certain filters, in particular CSS compilers like SASS, allow bundle
  9. source files to reference additional files which the user may not have listed
  10. in the bundle definition. The bundles support an additional ``depends``
  11. argument that can list files that should be watched for modification.
  12. Second, if the bundle definition itself changes, i.e., source files being added
  13. or removed, or the list of applied filters modified, the bundle needs to be
  14. rebuilt also. Since there is no single fixed place where bundles are defined,
  15. simply watching the timestamp of that bundle definition file is not good enough.
  16. To solve the latter problem, we employ an environment-specific cache of bundle
  17. definitions.
  18. Note that there is no ``HashUpdater``. This doesn't make sense for two reasons.
  19. First, for a live system, it isn't fast enough. Second, for prebuilding assets,
  20. the cache is a superior solution for getting essentially the same speed
  21. increase as using the hash to reliably determine which bundles to skip.
  22. """
  23. from webassets import six
  24. from webassets.six.moves import map
  25. from webassets.six.moves import zip
  26. from webassets.exceptions import BundleError, BuildError
  27. from webassets.utils import RegistryMetaclass, is_url, hash_func
  28. __all__ = ('get_updater', 'SKIP_CACHE',
  29. 'TimestampUpdater', 'AlwaysUpdater',)
  30. SKIP_CACHE = object()
  31. """An updater can return this value as hint that a cache, if enabled,
  32. should probably not be used for the rebuild; This is currently used
  33. as a return value when a bundle's dependencies have changed, which
  34. would currently not cause a different cache key to be used.
  35. This is marked a hint, because in the future, the bundle may be smart
  36. enough to make this decision by itself.
  37. """
  38. class BaseUpdater(six.with_metaclass(RegistryMetaclass(
  39. clazz=lambda: BaseUpdater, attribute='needs_rebuild',
  40. desc='an updater implementation'))):
  41. """Base updater class.
  42. Child classes that define an ``id`` attribute are accessible via their
  43. string id in the configuration.
  44. A single instance can be used with different environments.
  45. """
  46. def needs_rebuild(self, bundle, ctx):
  47. """Returns ``True`` if the given bundle needs to be rebuilt,
  48. ``False`` otherwise.
  49. """
  50. raise NotImplementedError()
  51. def build_done(self, bundle, ctx):
  52. """This will be called once a bundle has been successfully built.
  53. """
  54. get_updater = BaseUpdater.resolve
  55. class BundleDefUpdater(BaseUpdater):
  56. """Supports the bundle definition cache update check that child
  57. classes are usually going to want to use also.
  58. """
  59. def check_bundle_definition(self, bundle, ctx):
  60. if not ctx.cache:
  61. # If no global cache is configured, we could always
  62. # fall back to a memory-cache specific for the rebuild
  63. # process (store as env._update_cache); however,
  64. # whenever a bundle definition changes, it's likely that
  65. # a process restart will be required also, so in most cases
  66. # this would make no sense.
  67. return False
  68. cache_key = ('bdef', bundle.output)
  69. current_hash = "%s" % hash_func(bundle)
  70. cached_hash = ctx.cache.get(cache_key)
  71. # This may seem counter-intuitive, but if no cache entry is found
  72. # then we actually return "no update needed". This is because
  73. # otherwise if no cache / a dummy cache is used, then we would be
  74. # rebuilding every single time.
  75. if not cached_hash is None:
  76. return cached_hash != current_hash
  77. return False
  78. def needs_rebuild(self, bundle, ctx):
  79. return self.check_bundle_definition(bundle, ctx)
  80. def build_done(self, bundle, ctx):
  81. if not ctx.cache:
  82. return False
  83. cache_key = ('bdef', bundle.output)
  84. cache_value = "%s" % hash_func(bundle)
  85. ctx.cache.set(cache_key, cache_value)
  86. class TimestampUpdater(BundleDefUpdater):
  87. id = 'timestamp'
  88. def check_timestamps(self, bundle, ctx, o_modified=None):
  89. from .bundle import Bundle
  90. from webassets.version import TimestampVersion
  91. if not o_modified:
  92. try:
  93. resolved_output = bundle.resolve_output(ctx)
  94. except BundleError:
  95. # This exception will occur when the bundle output has
  96. # placeholder, but a version cannot be found. If the
  97. # user has defined a manifest, this will just be the first
  98. # build. Return True to let it happen.
  99. # However, if no manifest is defined, raise an error,
  100. # because otherwise, this updater would always return True,
  101. # and thus not do its job at all.
  102. if ctx.manifest is None:
  103. raise BuildError((
  104. '%s uses a version placeholder, and you are '
  105. 'using "%s" versions. To use automatic '
  106. 'building in this configuration, you need to '
  107. 'define a manifest.' % (bundle, ctx.versions)))
  108. return True
  109. try:
  110. o_modified = TimestampVersion.get_timestamp(resolved_output)
  111. except OSError:
  112. # If the output file does not exist, we'll have to rebuild
  113. return True
  114. # Recurse through the bundle hierarchy. Check the timestamp of all
  115. # the bundle source files, as well as any additional
  116. # dependencies that we are supposed to watch.
  117. from webassets.bundle import wrap
  118. for iterator, result in (
  119. (lambda e: map(lambda s: s[1], bundle.resolve_contents(e)), True),
  120. (bundle.resolve_depends, SKIP_CACHE)
  121. ):
  122. for item in iterator(ctx):
  123. if isinstance(item, Bundle):
  124. nested_result = self.check_timestamps(item, wrap(ctx, item), o_modified)
  125. if nested_result:
  126. return nested_result
  127. elif not is_url(item):
  128. try:
  129. s_modified = TimestampVersion.get_timestamp(item)
  130. except OSError:
  131. # If a file goes missing, always require
  132. # a rebuild.
  133. return result
  134. else:
  135. if s_modified > o_modified:
  136. return result
  137. return False
  138. def needs_rebuild(self, bundle, ctx):
  139. return \
  140. super(TimestampUpdater, self).needs_rebuild(bundle, ctx) or \
  141. self.check_timestamps(bundle, ctx)
  142. def build_done(self, bundle, ctx):
  143. # Reset the resolved dependencies, so any globs will be
  144. # re-resolved the next time we check if a rebuild is
  145. # required. This ensures that we begin watching new files
  146. # that are created, while still caching the globs as long
  147. # no changes happen.
  148. bundle._resolved_depends = None
  149. super(TimestampUpdater, self).build_done(bundle, ctx)
  150. class AlwaysUpdater(BaseUpdater):
  151. id = 'always'
  152. def needs_rebuild(self, bundle, ctx):
  153. return True