Bug 884587 - Part 1: Perform file removal with purge manifests; r=glandium
This commit is contained in:
26
Makefile.in
26
Makefile.in
@@ -38,15 +38,29 @@ DIST_GARBAGE = config.cache config.log config.status* config-defs.h \
|
|||||||
$(topsrcdir)/.mozconfig.mk $(topsrcdir)/.mozconfig.out
|
$(topsrcdir)/.mozconfig.mk $(topsrcdir)/.mozconfig.out
|
||||||
|
|
||||||
ifndef MOZ_PROFILE_USE
|
ifndef MOZ_PROFILE_USE
|
||||||
|
# One of the first things we do in the build is purge "unknown" files
|
||||||
|
# from the object directory. This serves two purposes:
|
||||||
|
#
|
||||||
|
# 1) Remove files from a previous build no longer accounted for in
|
||||||
|
# this build configuration.
|
||||||
|
#
|
||||||
|
# 2) Work around poor build system dependencies by forcing some
|
||||||
|
# rebuilds.
|
||||||
|
#
|
||||||
|
# Ideally #2 does not exist. Our reliance on this aspect should diminish
|
||||||
|
# over time.
|
||||||
|
#
|
||||||
|
# moz.build backend generation simply installs a set of "manifests" into
|
||||||
|
# a common directory. Each manifest is responsible for defining files in
|
||||||
|
# a specific subdirectory of the object directory. The invoked Python
|
||||||
|
# script simply iterates over all the manifests, purging files as
|
||||||
|
# necessary. To manage new directories or add files to the manifests,
|
||||||
|
# modify the backend generator.
|
||||||
|
#
|
||||||
# We need to explicitly put backend.RecursiveMakeBackend.built here
|
# We need to explicitly put backend.RecursiveMakeBackend.built here
|
||||||
# otherwise the rule in rules.mk doesn't run early enough.
|
# otherwise the rule in rules.mk doesn't run early enough.
|
||||||
default alldep all:: CLOBBER $(topsrcdir)/configure config.status backend.RecursiveMakeBackend.built
|
default alldep all:: CLOBBER $(topsrcdir)/configure config.status backend.RecursiveMakeBackend.built
|
||||||
$(RM) -r $(DIST)/sdk
|
$(PYTHON) $(topsrcdir)/config/purge_directories.py -d _build_manifests/purge .
|
||||||
$(RM) -r $(DIST)/include
|
|
||||||
$(RM) -r $(DIST)/private
|
|
||||||
$(RM) -r $(DIST)/public
|
|
||||||
$(RM) -r $(DIST)/bin
|
|
||||||
$(RM) -r _tests
|
|
||||||
endif
|
endif
|
||||||
|
|
||||||
CLOBBER: $(topsrcdir)/CLOBBER
|
CLOBBER: $(topsrcdir)/CLOBBER
|
||||||
|
|||||||
77
config/purge_directories.py
Normal file
77
config/purge_directories.py
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
# This script is used to purge a directory of unwanted files as defined by
|
||||||
|
# a manifest file.
|
||||||
|
|
||||||
|
from __future__ import print_function, unicode_literals
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import threading
|
||||||
|
|
||||||
|
from mozpack.manifests import PurgeManifest
|
||||||
|
|
||||||
|
def do_purge(purger, dest, state):
|
||||||
|
state['result'] = purger.purge(dest)
|
||||||
|
|
||||||
|
def process_manifest(topdir, manifest_path):
|
||||||
|
manifest = PurgeManifest.from_path(manifest_path)
|
||||||
|
purger = manifest.get_purger()
|
||||||
|
full = os.path.join(topdir, manifest.relpath)
|
||||||
|
|
||||||
|
state = dict(
|
||||||
|
relpath=manifest.relpath,
|
||||||
|
result=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
t = threading.Thread(target=do_purge, args=(purger, full, state))
|
||||||
|
state['thread'] = t
|
||||||
|
t.start()
|
||||||
|
|
||||||
|
return state
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description='Purge a directory of untracked files.')
|
||||||
|
|
||||||
|
parser.add_argument('--directory', '-d',
|
||||||
|
help='Directory containing manifest files. Will process every file '
|
||||||
|
'in directory.')
|
||||||
|
parser.add_argument('topdir',
|
||||||
|
help='Top directory all paths are evaluated from.')
|
||||||
|
parser.add_argument('manifests', nargs='*',
|
||||||
|
help='List of manifest files defining purge operations to perform.')
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
states = []
|
||||||
|
|
||||||
|
print('Purging unaccounted files from object directory...')
|
||||||
|
|
||||||
|
# We perform purging using threads for performance reasons. Hopefully
|
||||||
|
# multiple I/O operations will be faster than just 1.
|
||||||
|
paths = []
|
||||||
|
if args.directory:
|
||||||
|
for path in sorted(os.listdir(args.directory)):
|
||||||
|
paths.append(os.path.join(args.directory, path))
|
||||||
|
|
||||||
|
paths.extend(args.manifests)
|
||||||
|
|
||||||
|
for path in paths:
|
||||||
|
states.append(process_manifest(args.topdir, path))
|
||||||
|
|
||||||
|
for state in states:
|
||||||
|
state['thread'].join()
|
||||||
|
print('Deleted %d files and %d directories from %s.' % (
|
||||||
|
state['result'].removed_files_count,
|
||||||
|
state['result'].removed_directories_count,
|
||||||
|
state['relpath']
|
||||||
|
))
|
||||||
|
|
||||||
|
print('Finished purging.')
|
||||||
|
|
||||||
|
sys.exit(0)
|
||||||
@@ -9,6 +9,9 @@ import logging
|
|||||||
import os
|
import os
|
||||||
import types
|
import types
|
||||||
|
|
||||||
|
from mozpack.copier import FilePurger
|
||||||
|
from mozpack.manifests import PurgeManifest
|
||||||
|
|
||||||
from .base import BuildBackend
|
from .base import BuildBackend
|
||||||
from ..frontend.data import (
|
from ..frontend.data import (
|
||||||
ConfigFileSubstitution,
|
ConfigFileSubstitution,
|
||||||
@@ -127,6 +130,15 @@ class RecursiveMakeBackend(BuildBackend):
|
|||||||
self.backend_input_files.add(os.path.join(self.environment.topobjdir,
|
self.backend_input_files.add(os.path.join(self.environment.topobjdir,
|
||||||
'config', 'autoconf.mk'))
|
'config', 'autoconf.mk'))
|
||||||
|
|
||||||
|
self._purge_manifests = dict(
|
||||||
|
dist_bin=PurgeManifest(relpath='dist/bin'),
|
||||||
|
dist_include=PurgeManifest(relpath='dist/include'),
|
||||||
|
dist_private=PurgeManifest(relpath='dist/private'),
|
||||||
|
dist_public=PurgeManifest(relpath='dist/public'),
|
||||||
|
dist_sdk=PurgeManifest(relpath='dist/sdk'),
|
||||||
|
tests=PurgeManifest(relpath='_tests'),
|
||||||
|
)
|
||||||
|
|
||||||
def _update_from_avoid_write(self, result):
|
def _update_from_avoid_write(self, result):
|
||||||
existed, updated = result
|
existed, updated = result
|
||||||
|
|
||||||
@@ -252,6 +264,8 @@ class RecursiveMakeBackend(BuildBackend):
|
|||||||
self._update_from_avoid_write(mastermanifest.close())
|
self._update_from_avoid_write(mastermanifest.close())
|
||||||
self.summary.managed_count += 1
|
self.summary.managed_count += 1
|
||||||
|
|
||||||
|
self._write_purge_manifests()
|
||||||
|
|
||||||
def _process_directory_traversal(self, obj, backend_file):
|
def _process_directory_traversal(self, obj, backend_file):
|
||||||
"""Process a data.DirectoryTraversal instance."""
|
"""Process a data.DirectoryTraversal instance."""
|
||||||
fh = backend_file.fh
|
fh = backend_file.fh
|
||||||
@@ -323,3 +337,27 @@ class RecursiveMakeBackend(BuildBackend):
|
|||||||
if obj.relativedir != '':
|
if obj.relativedir != '':
|
||||||
manifest = '%s/%s' % (obj.relativedir, manifest)
|
manifest = '%s/%s' % (obj.relativedir, manifest)
|
||||||
self.xpcshell_manifests.append(manifest)
|
self.xpcshell_manifests.append(manifest)
|
||||||
|
|
||||||
|
def _write_purge_manifests(self):
|
||||||
|
# We write out a "manifest" file for each directory that is to be
|
||||||
|
# purged.
|
||||||
|
#
|
||||||
|
# Ideally we have as few manifests as possible - ideally only 1. This
|
||||||
|
# will likely require all build metadata to be in emitted objects.
|
||||||
|
# We're not quite there yet, so we maintain multiple manifests.
|
||||||
|
man_dir = os.path.join(self.environment.topobjdir, '_build_manifests',
|
||||||
|
'purge')
|
||||||
|
|
||||||
|
# We have a purger for the manifests themselves to ensure we don't over
|
||||||
|
# purge if we delete a purge manifest.
|
||||||
|
purger = FilePurger()
|
||||||
|
|
||||||
|
for k, manifest in self._purge_manifests.items():
|
||||||
|
purger.add(k)
|
||||||
|
full = os.path.join(man_dir, k)
|
||||||
|
|
||||||
|
fh = FileAvoidWrite(os.path.join(man_dir, k))
|
||||||
|
manifest.write_fileobj(fh)
|
||||||
|
self._update_from_avoid_write(fh.close())
|
||||||
|
|
||||||
|
purger.purge(man_dir)
|
||||||
|
|||||||
@@ -89,15 +89,17 @@ class BackendTester(unittest.TestCase):
|
|||||||
config['substs'].append(('top_srcdir', srcdir))
|
config['substs'].append(('top_srcdir', srcdir))
|
||||||
return ConfigEnvironment(srcdir, objdir, **config)
|
return ConfigEnvironment(srcdir, objdir, **config)
|
||||||
|
|
||||||
def _emit(self, name):
|
def _emit(self, name, env=None):
|
||||||
|
if not env:
|
||||||
env = self._get_environment(name)
|
env = self._get_environment(name)
|
||||||
|
|
||||||
reader = BuildReader(env)
|
reader = BuildReader(env)
|
||||||
emitter = TreeMetadataEmitter(env)
|
emitter = TreeMetadataEmitter(env)
|
||||||
|
|
||||||
return env, emitter.emit(reader.read_topsrcdir())
|
return env, emitter.emit(reader.read_topsrcdir())
|
||||||
|
|
||||||
def _consume(self, name, cls):
|
def _consume(self, name, cls, env=None):
|
||||||
env, objs = self._emit(name)
|
env, objs = self._emit(name, env=env)
|
||||||
backend = cls(env)
|
backend = cls(env)
|
||||||
backend.consume(objs)
|
backend.consume(objs)
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ from __future__ import unicode_literals
|
|||||||
import os
|
import os
|
||||||
import time
|
import time
|
||||||
|
|
||||||
|
from mozpack.manifests import PurgeManifest
|
||||||
from mozunit import main
|
from mozunit import main
|
||||||
|
|
||||||
from mozbuild.backend.configenvironment import ConfigEnvironment
|
from mozbuild.backend.configenvironment import ConfigEnvironment
|
||||||
@@ -269,5 +270,42 @@ class TestRecursiveMakeBackend(BackendTester):
|
|||||||
'; THIS FILE WAS AUTOMATICALLY GENERATED. DO NOT MODIFY BY HAND.',
|
'; THIS FILE WAS AUTOMATICALLY GENERATED. DO NOT MODIFY BY HAND.',
|
||||||
''] + ['[include:%s/xpcshell.ini]' % x for x in expected])
|
''] + ['[include:%s/xpcshell.ini]' % x for x in expected])
|
||||||
|
|
||||||
|
def test_purge_manifests_written(self):
|
||||||
|
env = self._consume('stub0', RecursiveMakeBackend)
|
||||||
|
|
||||||
|
purge_dir = os.path.join(env.topobjdir, '_build_manifests', 'purge')
|
||||||
|
self.assertTrue(os.path.exists(purge_dir))
|
||||||
|
|
||||||
|
expected = [
|
||||||
|
'dist_bin',
|
||||||
|
'dist_include',
|
||||||
|
'dist_private',
|
||||||
|
'dist_public',
|
||||||
|
'dist_sdk',
|
||||||
|
'tests',
|
||||||
|
]
|
||||||
|
|
||||||
|
for e in expected:
|
||||||
|
full = os.path.join(purge_dir, e)
|
||||||
|
self.assertTrue(os.path.exists(full))
|
||||||
|
|
||||||
|
m = PurgeManifest.from_path(os.path.join(purge_dir, 'dist_bin'))
|
||||||
|
self.assertEqual(m.relpath, 'dist/bin')
|
||||||
|
|
||||||
|
def test_old_purge_manifest_deleted(self):
|
||||||
|
# Simulate a purge manifest from a previous backend version. Ensure it
|
||||||
|
# is deleted.
|
||||||
|
env = self._get_environment('stub0')
|
||||||
|
purge_dir = os.path.join(env.topobjdir, '_build_manifests', 'purge')
|
||||||
|
manifest_path = os.path.join(purge_dir, 'old_manifest')
|
||||||
|
os.makedirs(purge_dir)
|
||||||
|
m = PurgeManifest()
|
||||||
|
m.write_file(manifest_path)
|
||||||
|
|
||||||
|
self.assertTrue(os.path.exists(manifest_path))
|
||||||
|
self._consume('stub0', RecursiveMakeBackend, env)
|
||||||
|
self.assertFalse(os.path.exists(manifest_path))
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
main()
|
main()
|
||||||
|
|||||||
89
python/mozbuild/mozpack/manifests.py
Normal file
89
python/mozbuild/mozpack/manifests.py
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from .copier import FilePurger
|
||||||
|
import mozpack.path as mozpath
|
||||||
|
|
||||||
|
|
||||||
|
class UnreadablePurgeManifest(Exception):
|
||||||
|
"""Error for failure when reading content of a serialized PurgeManifest."""
|
||||||
|
|
||||||
|
|
||||||
|
class PurgeManifest(object):
|
||||||
|
"""Describes actions to be used with a copier.FilePurger instance.
|
||||||
|
|
||||||
|
This class facilitates serialization and deserialization of data used
|
||||||
|
to construct a copier.FilePurger and to perform a purge operation.
|
||||||
|
|
||||||
|
The manifest contains a set of entries (paths that are accounted for and
|
||||||
|
shouldn't be purged) and a relative path. The relative path is optional and
|
||||||
|
can be used to e.g. have several manifest files in a directory be
|
||||||
|
dynamically applied to subdirectories under a common base directory.
|
||||||
|
|
||||||
|
Don't be confused by the name of this class: entries are files that are
|
||||||
|
*not* purged.
|
||||||
|
"""
|
||||||
|
def __init__(self, relpath=''):
|
||||||
|
self.relpath = relpath
|
||||||
|
self.entries = set()
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
if not isinstance(other, PurgeManifest):
|
||||||
|
return False
|
||||||
|
|
||||||
|
return other.relpath == self.relpath and other.entries == self.entries
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def from_path(path):
|
||||||
|
with open(path, 'rt') as fh:
|
||||||
|
return PurgeManifest.from_fileobj(fh)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def from_fileobj(fh):
|
||||||
|
m = PurgeManifest()
|
||||||
|
|
||||||
|
version = fh.readline().rstrip()
|
||||||
|
if version != '1':
|
||||||
|
raise UnreadablePurgeManifest('Unknown manifest version: ' %
|
||||||
|
version)
|
||||||
|
|
||||||
|
m.relpath = fh.readline().rstrip()
|
||||||
|
|
||||||
|
for entry in fh:
|
||||||
|
m.entries.add(entry.rstrip())
|
||||||
|
|
||||||
|
return m
|
||||||
|
|
||||||
|
def add(self, path):
|
||||||
|
return self.entries.add(path)
|
||||||
|
|
||||||
|
def write_file(self, path):
|
||||||
|
with open(path, 'wt') as fh:
|
||||||
|
return self.write_fileobj(fh)
|
||||||
|
|
||||||
|
def write_fileobj(self, fh):
|
||||||
|
fh.write('1\n')
|
||||||
|
fh.write('%s\n' % self.relpath)
|
||||||
|
|
||||||
|
# We write sorted so written output is consistent.
|
||||||
|
for entry in sorted(self.entries):
|
||||||
|
fh.write('%s\n' % entry)
|
||||||
|
|
||||||
|
def get_purger(self, prepend_relpath=False):
|
||||||
|
"""Obtain a FilePurger instance from this manifest.
|
||||||
|
|
||||||
|
If :prepend_relpath is truish, the relative path in the manifest will
|
||||||
|
be prepended to paths added to the FilePurger. Otherwise, the raw paths
|
||||||
|
will be used.
|
||||||
|
"""
|
||||||
|
p = FilePurger()
|
||||||
|
for entry in self.entries:
|
||||||
|
if prepend_relpath:
|
||||||
|
entry = mozpath.join(self.relpath, entry)
|
||||||
|
|
||||||
|
p.add(entry)
|
||||||
|
|
||||||
|
return p
|
||||||
48
python/mozbuild/mozpack/test/test_manifests.py
Normal file
48
python/mozbuild/mozpack/test/test_manifests.py
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import os
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
import mozunit
|
||||||
|
|
||||||
|
from mozpack.manifests import (
|
||||||
|
PurgeManifest,
|
||||||
|
UnreadablePurgeManifest,
|
||||||
|
)
|
||||||
|
from mozpack.test.test_files import TestWithTmpDir
|
||||||
|
|
||||||
|
|
||||||
|
class TestPurgeManifest(TestWithTmpDir):
|
||||||
|
def test_construct(self):
|
||||||
|
m = PurgeManifest()
|
||||||
|
self.assertEqual(m.relpath, '')
|
||||||
|
self.assertEqual(len(m.entries), 0)
|
||||||
|
|
||||||
|
def test_serialization(self):
|
||||||
|
m = PurgeManifest(relpath='rel')
|
||||||
|
m.add('foo')
|
||||||
|
m.add('bar')
|
||||||
|
p = self.tmppath('m')
|
||||||
|
m.write_file(p)
|
||||||
|
|
||||||
|
self.assertTrue(os.path.exists(p))
|
||||||
|
|
||||||
|
m2 = PurgeManifest.from_path(p)
|
||||||
|
self.assertEqual(m.relpath, m2.relpath)
|
||||||
|
self.assertEqual(m.entries, m2.entries)
|
||||||
|
self.assertEqual(m, m2)
|
||||||
|
|
||||||
|
def test_unknown_version(self):
|
||||||
|
p = self.tmppath('bad')
|
||||||
|
|
||||||
|
with open(p, 'wt') as fh:
|
||||||
|
fh.write('2\n')
|
||||||
|
fh.write('not relevant')
|
||||||
|
|
||||||
|
with self.assertRaises(UnreadablePurgeManifest):
|
||||||
|
PurgeManifest.from_path(p)
|
||||||
|
|
||||||
Reference in New Issue
Block a user