import concurrent.futures as futures
import errno
import os
import stat
import sys
from collections import Counter, OrderedDict, defaultdict
import mozpack.path as mozpath
from mozpack.errors import errors
from mozpack.files import BaseFile, Dest
class FileRegistry:
def __init__(self):
self._files = OrderedDict()
self._required_directories = Counter()
self._partial_paths_cache = {}
def _partial_paths(self, path):
dir_name = path.rpartition("/")[0]
if not dir_name:
return []
partial_paths = self._partial_paths_cache.get(dir_name)
if partial_paths:
return partial_paths
partial_paths = [dir_name] + self._partial_paths(dir_name)
self._partial_paths_cache[dir_name] = partial_paths
return partial_paths
def add(self, path, content):
assert isinstance(content, BaseFile)
if path in self._files:
return errors.error("%s already added" % path)
if self._required_directories[path] > 0:
return errors.error("Can't add %s: it is a required directory" % path)
partial_paths = self._partial_paths(path)
for partial_path in partial_paths:
if partial_path in self._files:
return errors.error("Can't add %s: %s is a file" % (path, partial_path))
self._files[path] = content
self._required_directories.update(partial_paths)
def match(self, pattern):
if "*" in pattern:
return [p for p in self.paths() if mozpath.match(p, pattern)]
if pattern == "":
return self.paths()
if pattern in self._files:
return [pattern]
return [p for p in self.paths() if mozpath.basedir(p, [pattern]) == pattern]
def remove(self, pattern):
items = self.match(pattern)
if not items:
return errors.error(
"Can't remove %s: %s"
% (pattern, "not matching anything previously added")
)
for i in items:
del self._files[i]
self._required_directories.subtract(self._partial_paths(i))
def paths(self):
return list(self._files)
def __len__(self):
return len(self._files)
def __contains__(self, pattern):
raise RuntimeError(
"'in' operator forbidden for %s. Use contains()." % self.__class__.__name__
)
def contains(self, pattern):
return len(self.match(pattern)) > 0
def __getitem__(self, path):
return self._files[path]
def __iter__(self):
return iter(self._files.items())
def required_directories(self):
return set(k for k, v in self._required_directories.items() if v > 0)
def output_to_inputs_tree(self):
tree = {}
for output, file in self:
output = mozpath.normpath(output)
tree[output] = set(mozpath.normpath(f) for f in file.inputs())
return tree
def input_to_outputs_tree(self):
tree = defaultdict(set)
for output, file in self:
output = mozpath.normpath(output)
for input in file.inputs():
input = mozpath.normpath(input)
tree[input].add(output)
return dict(tree)
class FileRegistrySubtree:
def __new__(cls, base, registry):
if not base:
return registry
return object.__new__(cls)
def __init__(self, base, registry):
self._base = base
self._registry = registry
def _get_path(self, path):
return mozpath.join(self._base, path) if path else self._base
def add(self, path, content):
return self._registry.add(self._get_path(path), content)
def match(self, pattern):
return [
mozpath.relpath(p, self._base)
for p in self._registry.match(self._get_path(pattern))
]
def remove(self, pattern):
return self._registry.remove(self._get_path(pattern))
def paths(self):
return [p for p, f in self]
def __len__(self):
return len(self.paths())
def contains(self, pattern):
return self._registry.contains(self._get_path(pattern))
def __getitem__(self, path):
return self._registry[self._get_path(path)]
def __iter__(self):
for p, f in self._registry:
if mozpath.basedir(p, [self._base]):
yield mozpath.relpath(p, self._base), f
class FileCopyResult:
def __init__(self):
self.updated_files = set()
self.existing_files = set()
self.removed_files = set()
self.removed_directories = set()
@property
def updated_files_count(self):
return len(self.updated_files)
@property
def existing_files_count(self):
return len(self.existing_files)
@property
def removed_files_count(self):
return len(self.removed_files)
@property
def removed_directories_count(self):
return len(self.removed_directories)
class FileCopier(FileRegistry):
def copy(
self,
destination,
skip_if_older=True,
remove_unaccounted=True,
remove_all_directory_symlinks=True,
remove_empty_directories=True,
):
assert isinstance(destination, str)
assert not os.path.exists(destination) or os.path.isdir(destination)
result = FileCopyResult()
have_symlinks = hasattr(os, "symlink")
destination = os.path.normpath(destination)
os.makedirs(destination, exist_ok=True)
required_dirs = set([destination])
required_dirs |= set(
os.path.normpath(os.path.join(destination, d))
for d in self.required_directories()
)
for d in sorted(required_dirs, key=len):
os.makedirs(d, exist_ok=True)
if have_symlinks and d != destination:
st = os.lstat(d)
if stat.S_ISLNK(st.st_mode):
os.remove(d)
os.mkdir(d)
if not os.access(d, os.W_OK):
umask = os.umask(0o077)
os.umask(umask)
os.chmod(d, 0o777 & ~umask)
if isinstance(remove_unaccounted, FileRegistry):
existing_files = set(
os.path.normpath(os.path.join(destination, p))
for p in remove_unaccounted.paths()
)
existing_dirs = set(
os.path.normpath(os.path.join(destination, p))
for p in remove_unaccounted.required_directories()
)
existing_dirs |= {os.path.normpath(destination)}
else:
existing_dirs = set()
existing_files = set()
for root, dirs, files in os.walk(destination):
if have_symlinks:
filtered = []
for d in dirs:
full = os.path.join(root, d)
st = os.lstat(full)
if stat.S_ISLNK(st.st_mode):
if remove_all_directory_symlinks:
os.remove(full)
result.removed_files.add(os.path.normpath(full))
else:
existing_files.add(os.path.normpath(full))
else:
filtered.append(d)
dirs[:] = filtered
existing_dirs.add(os.path.normpath(root))
for d in dirs:
existing_dirs.add(os.path.normpath(os.path.join(root, d)))
for f in files:
existing_files.add(os.path.normpath(os.path.join(root, f)))
dest_files = set()
copy_results = []
if sys.platform == "win32" and len(self) > 100:
with futures.ThreadPoolExecutor(4) as e:
fs = []
for p, f in self:
destfile = os.path.normpath(os.path.join(destination, p))
fs.append((destfile, e.submit(f.copy, destfile, skip_if_older)))
copy_results = [(path, f.result) for path, f in fs]
else:
for p, f in self:
destfile = os.path.normpath(os.path.join(destination, p))
copy_results.append((destfile, f.copy(destfile, skip_if_older)))
for destfile, copy_result in copy_results:
dest_files.add(destfile)
if copy_result:
result.updated_files.add(destfile)
else:
result.existing_files.add(destfile)
if remove_unaccounted:
for f in existing_files - dest_files:
if os.name == "nt" and not os.access(f, os.W_OK):
os.chmod(f, 0o600)
os.remove(f)
result.removed_files.add(f)
if not remove_empty_directories:
return result
remove_dirs = existing_dirs - required_dirs
if not remove_unaccounted:
parents = set()
pathsep = os.path.sep
for f in existing_files:
path = f
while True:
dirname = path.rpartition(pathsep)[0]
if dirname in parents:
break
parents.add(dirname)
path = dirname
remove_dirs -= parents
for d in sorted(remove_dirs, key=len, reverse=True):
try:
try:
os.rmdir(d)
except OSError as e:
if e.errno in (errno.EPERM, errno.EACCES):
os.chmod(d, 0o700)
os.rmdir(d)
else:
raise
except OSError as e:
if (
isinstance(remove_unaccounted, FileRegistry)
and e.errno == errno.ENOTEMPTY
):
continue
raise
result.removed_directories.add(d)
return result
class Jarrer(FileRegistry, BaseFile):
def __init__(self, compress=True):
self.compress = compress
self._preload = []
self._compress_options = {} FileRegistry.__init__(self)
def add(self, path, content, compress=None):
FileRegistry.add(self, path, content)
if compress is not None:
self._compress_options[path] = compress
def copy(self, dest, skip_if_older=True):
class DeflaterDest(Dest):
def __init__(self, orig=None, compress=True):
self.mode = None
self.deflater = orig
self.compress = compress
def read(self, length=-1):
if self.mode != "r":
assert self.mode is None
self.mode = "r"
return self.deflater.read(length)
def write(self, data):
if self.mode != "w":
from mozpack.mozjar import Deflater
self.deflater = Deflater(self.compress)
self.mode = "w"
self.deflater.write(data)
def exists(self):
return self.deflater is not None
if isinstance(dest, str):
dest = Dest(dest)
assert isinstance(dest, Dest)
from mozpack.mozjar import JarReader, JarWriter
try:
old_jar = JarReader(fileobj=dest)
except Exception:
old_jar = []
old_contents = dict([(f.filename, f) for f in old_jar])
with JarWriter(fileobj=dest, compress=self.compress) as jar:
for path, file in self:
compress = self._compress_options.get(path, self.compress)
if path in old_contents:
deflater = DeflaterDest(old_contents[path], compress)
else:
deflater = DeflaterDest(compress=compress)
file.copy(deflater, skip_if_older)
jar.add(path, deflater.deflater, mode=file.mode, compress=compress)
if self._preload:
jar.preload(self._preload)
def open(self):
raise RuntimeError("unsupported")
def preload(self, paths):
self._preload.extend(paths)