import errno
import os
import shutil
import stat
import sys
from collections import Counter, OrderedDict, defaultdict
from concurrent import futures
import mozpack.path as mozpath
from mozpack.errors import errors
from mozpack.files import AbsoluteSymlinkFile, BaseFile, Dest, File
def _scandir_dest_info(top):
existing_files = set()
existing_dirs = set()
mtimes = {}
symlink_targets = {}
stack = [top]
while stack:
current = stack.pop()
existing_dirs.add(os.path.normpath(current))
try:
entries = os.scandir(current)
except OSError:
continue
with entries:
for entry in entries:
try:
normed = os.path.normpath(entry.path)
if entry.is_symlink():
existing_files.add(normed)
target = os.readlink(entry.path)
symlink_targets[normed] = mozpath.strip_extended_length_prefix(
target
)
elif entry.is_dir(follow_symlinks=False):
existing_dirs.add(normed)
stack.append(entry.path)
else:
existing_files.add(normed)
st = entry.stat(follow_symlinks=False)
mtimes[normed] = int(st.st_mtime * 1000)
except OSError:
continue
return existing_files, existing_dirs, mtimes, symlink_targets
def _collect_source_mtimes(copier):
by_dir = defaultdict(set)
for _, f in copier:
src_path = getattr(f, "path", None)
if src_path:
parent, name = os.path.split(os.path.normpath(src_path))
by_dir[parent].add(name)
mtimes = {}
src_dirs = set()
for dirpath, filenames in by_dir.items():
try:
it = os.scandir(dirpath)
except OSError:
continue
with it:
for entry in it:
if entry.name in filenames:
try:
normed = os.path.normpath(entry.path)
if entry.is_dir(follow_symlinks=True):
src_dirs.add(normed)
else:
st = entry.stat()
mtimes[normed] = int(st.st_mtime * 1000)
except OSError:
continue
return mtimes, src_dirs
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)
dest_mtimes = {}
dest_symlinks = {}
src_mtimes = {}
src_dirs = set()
src_future = None
if skip_if_older and len(self) > 100:
src_executor = futures.ThreadPoolExecutor(1)
src_future = src_executor.submit(_collect_source_mtimes, self)
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)}
if skip_if_older and len(self) > 100:
_, _, dest_mtimes, dest_symlinks = _scandir_dest_info(destination)
else:
existing_files, existing_dirs, dest_mtimes, dest_symlinks = (
_scandir_dest_info(destination)
)
if have_symlinks and remove_all_directory_symlinks:
for path in list(dest_symlinks):
if os.path.isdir(path):
os.remove(path)
result.removed_files.add(path)
existing_files.discard(path)
del dest_symlinks[path]
if src_future is not None:
src_mtimes, src_dirs = src_future.result()
src_executor.shutdown(wait=False)
dest_files = set()
files_to_copy = []
for p, f in self:
destfile = os.path.normpath(os.path.join(destination, p))
src_path = getattr(f, "path", None)
if (
isinstance(f, AbsoluteSymlinkFile)
and src_path
and os.path.normpath(src_path) in src_dirs
):
link_target = dest_symlinks.get(destfile)
if (
link_target is not None
and link_target == mozpath.strip_extended_length_prefix(src_path)
):
dest_files.add(destfile)
result.existing_files.add(destfile)
continue
try:
if os.path.lexists(destfile):
os.remove(destfile)
os.symlink(src_path, destfile)
dest_files.add(destfile)
result.updated_files.add(destfile)
except OSError:
if skip_if_older and os.path.isdir(destfile):
dest_files.add(destfile)
result.existing_files.add(destfile)
else:
shutil.copytree(src_path, destfile, dirs_exist_ok=True)
dest_files.add(destfile)
result.updated_files.add(destfile)
continue
if src_path and (dest_mtimes or dest_symlinks):
if isinstance(f, AbsoluteSymlinkFile):
link_target = dest_symlinks.get(destfile)
if (
link_target is not None
and link_target
== mozpath.strip_extended_length_prefix(src_path)
):
dest_files.add(destfile)
result.existing_files.add(destfile)
continue
if isinstance(f, AbsoluteSymlinkFile) or type(f) is File:
normed_src = os.path.normpath(src_path)
src_mtime = src_mtimes.get(normed_src)
dest_mtime = dest_mtimes.get(destfile)
if (
src_mtime is not None
and dest_mtime is not None
and src_mtime <= dest_mtime
):
dest_files.add(destfile)
result.existing_files.add(destfile)
continue
files_to_copy.append((destfile, f))
copy_results = []
if sys.platform == "win32" and len(files_to_copy) > 100:
with futures.ThreadPoolExecutor(4) as e:
fs = []
for destfile, f in files_to_copy:
fs.append((destfile, e.submit(f.copy, destfile, skip_if_older)))
copy_results = [(path, f.result()) for path, f in fs]
else:
for destfile, f in files_to_copy:
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)