blob: c28a2ebfc2fb25699220c8d7f11bd60dbdf2b4ef [file] [log] [blame]
#!/usr/bin/python
#
# Copyright (C) 2015 The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Merge packages into the target image."""
from __future__ import print_function
import argparse
import errno
import os
import re
import shutil
import sys
def makedirs(path):
"""Create all the dirs for |path| ignoring already existing errors."""
try:
os.makedirs(path)
except OSError as e:
if e.errno != errno.EEXIST:
raise
def unlink(path):
"""Delete |path| ignoring missing paths."""
try:
os.unlink(path)
except OSError as e:
if e.errno != errno.ENOENT:
raise
class Atom(object):
"""Hold details about a package atom (optionally in a vdb)."""
_PREFIX_RE = re.compile(r'^([>=<~!]*)(.*)$')
_PN_RE = r'(?P<package>[A-Za-z0-9_]+[A-Za-z0-9+_-]*?)'
_PV_RE = (r'(?P<version>[0-9](\.[0-9]+)*(_(pre|p|beta|alpha|rc)[0-9]*)*)'
r'(-r(?P<revision>[0-9]+))?')
_PN_PV_RE = re.compile(r'^%s(-%s)?$' % (_PN_RE, _PV_RE))
def __init__(self, atom, path=None):
self.atom = atom
self.path = path
# Extract any version matching prefixes.
m = self._PREFIX_RE.match(atom)
self.prefix, atom = m.groups()
# Split off trailing slot -- must be before category for subslots.
if ':' in atom:
atom, self._slot = atom.split(':', 1)
else:
self._slot = None
# Split off leading category.
if '/' in atom:
self.category, atom = atom.split('/', 1)
else:
self.category = None
# Split apart the name & version.
m = self._PN_PV_RE.match(atom)
self.pn = m.group('package')
self.pv = m.group('version')
self.rev = m.group('revision')
def _load_vdb_entry(self, entry):
if not self.path:
return None
path = os.path.join(self.path, entry)
with open(path) as f:
return f.read().strip()
@property
def slot(self):
if self._slot is None and self.path:
self._slot = self._load_vdb_entry('SLOT')
return self._slot
def load_contents(self, path_filter=None):
"""Read the CONTENTS file for |pkg| from |root|."""
if not path_filter:
path_filter = lambda x: True
ret = []
for line in self._load_vdb_entry('CONTENTS').splitlines():
line = line.strip()
if not line:
continue
typ, data = line.split(' ', 1)
if typ == 'obj':
path, _hash, _mtime = data.rsplit(' ', 2)
if path_filter(path):
ret.append(('obj', path))
elif typ == 'sym':
source, target = data.split(' -> ', 1)
target, _mtime = target.rsplit(' ', 1)
if path_filter(source):
ret.append(('sym', source, target))
elif typ == 'dir':
pass
else:
raise Exception('Unhandled entry: %s' % line)
return ret
def match(self, atom):
"""See if |atom| matches us."""
# TODO: Support the range matching like >=.
if self.prefix not in (None, '', '=', '~'):
raise ValueError('Only exact matches supported (not %s)'
% (self.prefix))
# Package names must always exist & match.
if atom.pn != self.pn:
return False
# Only require the category to match if both atoms specify it.
if len(set((atom.category, self.category, None))) == 3:
return False
# Only require the slot to match if both atoms specify it.
if self.slot and len(set((atom.slot, self.slot, None))) == 3:
return False
# Only require the version to match if both atoms specify it.
if len(set((atom.pv, self.pv, None))) == 3:
return False
# Only require the revision to match if both atoms specify it.
if (self.prefix is not '~' and
len(set((atom.rev, self.rev, None)))) == 3:
return False
# If we're still here, there's nothing left to say :).
return True
def __str__(self):
return ('Atom(%s/%s-%s-r%s:%s)' %
(self.category, self.pn, self.pv, self.rev, self.slot))
class Vdb(object):
"""Hold details about a vdb (a database of installed packages)."""
# https://projects.gentoo.org/pms/6/pms.html#x1-180003.1.1
CATEGORY_RE = re.compile(r'^[A-Za-z0-9_]+[A-Za-z0-9+_.-]*$')
# https://projects.gentoo.org/pms/6/pms.html#x1-200003.1.2
PACKAGE_RE = re.compile(r'^[A-Za-z0-9_]+[A-Za-z0-9+_-]*-[0-9]+')
def __init__(self, root):
self.pkgs = []
path = os.path.join(root, 'var', 'db', 'pkg')
for cat in os.listdir(path):
catdir = os.path.join(path, cat)
if not os.path.isdir(catdir) or not self.CATEGORY_RE.match(cat):
continue
for pkg in os.listdir(catdir):
pkgdir = os.path.join(catdir, pkg)
if (os.path.isdir(pkgdir) and
os.path.isfile(os.path.join(pkgdir, 'CONTENTS')) and
self.PACKAGE_RE.match(pkg)):
self.pkgs.append(Atom('%s/%s' % (cat, pkg), pkgdir))
def find_pkg(self, match_pkg):
"""Try to find the best match for |match_pkg|."""
match_atom = Atom(match_pkg)
for vdb_pkg in self.pkgs:
if match_atom.match(vdb_pkg):
return vdb_pkg
def ignore_files(path):
"""Figure out which files we actually care about."""
if path.startswith('/usr/share/doc'):
return False
elif path.startswith('/usr/share/gtk-doc'):
return False
elif path.startswith('/usr/share/info'):
return False
elif path.startswith('/usr/share/man'):
return False
elif path.startswith('/usr/include'):
return False
elif path.startswith('/usr/bin') and path.endswith('-config'):
return False
elif path.startswith('/usr/lib'):
if path.endswith('.a'):
return False
elif path.endswith('.pc'):
return False
return True
def merge(vdb, pkg, input_root, output_root, make_root, verbose=0):
"""Merge |pkg| from |input_root| to |output_root|."""
print('Merging %s ' % pkg, end='')
if verbose:
print('from %s to %s ' % (input_root, output_root))
# TODO: Grab a lock for |pkg|.
# First get the file listing to merge.
vdb_pkg = vdb.find_pkg(pkg)
if not vdb_pkg:
raise Exception('No packages found matching %s ' % pkg)
print('matched %s ' % vdb_pkg, end='')
contents = vdb_pkg.load_contents(path_filter=ignore_files)
# Now actually merge them.
for entry in contents:
if entry[0] == 'obj':
path = entry[1].lstrip('/')
output = os.path.join(output_root, path)
if verbose:
print('>>> %s' % path)
else:
print('.', end='')
makedirs(os.path.dirname(output))
if os.path.exists(output):
os.chmod(output, 0o666)
shutil.copy2(os.path.join(input_root, path), output)
elif entry[0] == 'sym':
path = entry[1].lstrip('/')
target = entry[2]
output = os.path.join(output_root, path)
if verbose:
print('>>> %s -> %s' % (path, target))
else:
print('.', end='')
unlink(output)
makedirs(os.path.dirname(output))
os.symlink(target, output)
else:
raise Exception('Unhandled entry: %r' % entry)
make_target = os.path.join(make_root, pkg + '.emerge')
makedirs(os.path.dirname(make_target))
open(make_target, 'w')
if not verbose:
print('')
def get_parser():
"""Return a command line parser."""
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('-p', '--package', action='append', required=True,
dest='packages',
help='Merge these package(s)')
parser.add_argument('--input-root', type=str, required=True,
help='Where to read the packages')
parser.add_argument('--output-root', type=str, required=True,
help='Where to write the packages')
parser.add_argument('--make-root', type=str, required=True,
help='Where make writes state files for rules')
parser.add_argument('-v', '--verbose', action='count',
help='Make output more verbose')
return parser
def main(argv):
parser = get_parser()
opts = parser.parse_args(argv)
vdb = Vdb(opts.input_root)
for pkg in opts.packages:
merge(vdb, pkg, opts.input_root, opts.output_root, opts.make_root,
verbose=opts.verbose)
if __name__ == '__main__':
main(sys.argv[1:])