#!/usr/bin/python # Copyright (c) 2015 The CoreOS Authors. All rights reserved. # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. from __future__ import unicode_literals import fnmatch import os import stat import sys import portage from portage import dep from portage import output from portage.dep.soname.SonameAtom import SonameAtom from portage.dep.soname.parse import parse_soname_deps VARDB = portage.db[portage.root]["vartree"].dbapi # TODO(marneam): possibly accept globs for arch and sonames IGNORE_MISSING = { # /usr/lib/go/src/debug/elf/testdata/gcc-386-freebsd-exec # /usr/lib/go/src/debug/elf/testdata/gcc-amd64-linux-exec "dev-lang/go": [SonameAtom("x86_32", "libc.so.6"), SonameAtom("x86_64", "libc.so.6")], "dev-lang/go-bootstrap": [SonameAtom("x86_32", "libc.so.6"), SonameAtom("x86_64", "libc.so.6")], # RPATHs and symlinks apparently confuse the perl-5.24 package "dev-lang/perl": [SonameAtom("x86_64", "libperl.so.5.26.2")], "sys-apps/texinfo": [SonameAtom("x86_64", "libperl.so.5.26")], # https://bugs.gentoo.org/show_bug.cgi?id=554582 "net-firewall/ebtables": [SonameAtom("x86_64", "libebt_802_3.so"), SonameAtom("x86_64", "libebt_among.so"), SonameAtom("x86_64", "libebt_arp.so"), SonameAtom("x86_64", "libebt_arpreply.so"), SonameAtom("x86_64", "libebt_ip.so"), SonameAtom("x86_64", "libebt_ip6.so"), SonameAtom("x86_64", "libebt_limit.so"), SonameAtom("x86_64", "libebt_log.so"), SonameAtom("x86_64", "libebt_mark.so"), SonameAtom("x86_64", "libebt_mark_m.so"), SonameAtom("x86_64", "libebt_nat.so"), SonameAtom("x86_64", "libebt_nflog.so"), SonameAtom("x86_64", "libebt_pkttype.so"), SonameAtom("x86_64", "libebt_redirect.so"), SonameAtom("x86_64", "libebt_standard.so"), SonameAtom("x86_64", "libebt_stp.so"), SonameAtom("x86_64", "libebt_ulog.so"), SonameAtom("x86_64", "libebt_vlan.so"), SonameAtom("x86_64", "libebtable_broute.so"), SonameAtom("x86_64", "libebtable_filter.so"), SonameAtom("x86_64", "libebtable_nat.so")], # Ignore the Rust libraries in their own libdir. "dev-libs/rustlib": [SonameAtom("x86_64", "librustc_data_structures.so"), SonameAtom("x86_64", "librustc_errors.so"), SonameAtom("x86_64", "libserialize.so"), SonameAtom("x86_64", "libstd.so"), SonameAtom("x86_64", "libsyntax.so"), SonameAtom("x86_64", "libsyntax_pos.so"), SonameAtom("x86_64", "libterm.so")], } USR_LINKS = ("/bin/", "/sbin/", "/lib/", "/lib32/", "/lib64/") IGNORE_SHEBANG = ( b"*/python[0-9].[0-9]/cgi.py", b"*/usr/lib64/modules/*/source/scripts/*", b"*/usr/share/nova-agent/*/etc/gentoo/nova-agent", b"*/tmp/*", b"*/Documentation/*", b"*/doc/*", ) IGNORE_SYMLINK = ( # symlinks to sdk chroot b"/build/*", b"/var/tmp/portage/*", b"/etc/portage/*", # symlinks to /run b"/usr/share/baselayout/motd", b"/etc/issue", b"/etc/motd", # Other b"/etc/lsb-release", # set later in the build process b"/usr/share/coreos", # set later in the build process b"/etc/coreos" # set later in the build process ) def provided_sonames(): for cpv in VARDB.cpv_all(): raw = VARDB.aux_get(cpv, ["PROVIDES"])[0] for atom in parse_soname_deps(raw): yield atom # soname.provided in PORTAGE_CONFIGROOT for atom in VARDB.settings.soname_provided: yield atom def ignore_sonames(cpv): for key in dep.match_to_list(cpv, IGNORE_MISSING.iterkeys()): for atom in IGNORE_MISSING[key]: yield atom def missing_sonames(): provided = frozenset(provided_sonames()) for cpv in VARDB.cpv_all(): raw = VARDB.aux_get(cpv, ["REQUIRES"])[0] requires = frozenset(parse_soname_deps(raw)) ignore = frozenset(ignore_sonames(cpv)) missing = requires - provided - ignore if missing: yield (cpv, missing) def usr_conflicts(): for cpv in VARDB.cpv_all(): raw = VARDB.aux_get(cpv, ["CONTENTS"])[0] usr = set() root = set() # format is: # obj /path goo 123 # dir /path/foo # sym /this -> that 123 # and so on for line in raw.split("\n"): if line[:4] != "obj " and line[:4] != "sym ": continue # yeah, hard to read, trying to make it fast... i = line.find("/", 5) topdir = line[4:i+1] if topdir == "/usr/": j = line.find("/", 9) nextdir = line[8:j+1] if nextdir in USR_LINKS: end = line.find(" ", 8) usr.add(line[8:end]) elif topdir in USR_LINKS: end = line.find(" ", 4) root.add(line[4:end]) conflicts = frozenset(root).intersection(usr) if conflicts: yield (cpv, conflicts) def check_libs(): ok = True for cpv, sonames in missing_sonames(): error("%s is missing libraries:", cpv) for soname in sonames: error("\t%s", soname) ok = False return ok def check_usr(): ok = True for cpv, conflicts in usr_conflicts(): error("%s has paths that conflict with /usr", cpv) for path in conflicts: error("\t%s", path) ok = False return ok def is_exe(path): # just check other, assuming root or group only commands are not scripts. perms = stat.S_IROTH | stat.S_IXOTH mode = os.lstat(path).st_mode return stat.S_ISREG(mode) and (mode & perms) == perms def check_shebang(): ok = True cache = {} root = os.environ.get("ROOT", b"/") for parent, _, files in os.walk(root): for path in [os.path.join(parent, f) for f in files]: if any(fnmatch.fnmatchcase(path, i) for i in IGNORE_SHEBANG): continue if not is_exe(path): continue with open(path, "r") as fd: line = fd.readline(80) if not line.startswith(b"#!"): continue args = line[2:].rstrip().split(None, 2) cmd = args.pop(0) if cmd in ('/usr/bin/env', '/bin/env') and args: prog = args.pop(0) if prog.startswith('-') and args: prog = args.pop(0) cmd = '(env)/%s' % prog if cmd not in cache: if cmd.startswith('(env)'): cache[cmd] = False for bindir in (root+'/usr/bin', root+'/usr/sbin'): if os.path.exists(os.path.join(bindir, prog)): cache[cmd] = True break else: cache[cmd] = os.path.exists(root+cmd) if not cache[cmd]: relpath = path[len(root):] error("%s: %s does not exist", relpath, cmd) ok = False return ok class chrooted(): """ chrooted provides a context so that it can be used via with. For example: with chrooted("/some/rootfs"): do_operations_in_rootfs() do_operations_not_in_rootfs() """ def __init__(self, path): self.path = path def __enter__(self): self.restore_fd = os.open(b"/", os.O_RDONLY) self.working_dir = os.getcwd() os.chroot(self.path) def __exit__(self, type, value, traceback): os.fchdir(self.restore_fd) os.chroot(b".") os.chdir(self.working_dir) os.close(self.restore_fd) def check_symlink(): if os.getuid() != 0: error("symlink check must be run as root (chroot)") return False ok = True root = os.environ.get("ROOT", b"/") with chrooted(root): for parent, dirs, files in os.walk(b"/"): for path in [os.path.join(parent, p) for p in files + dirs]: if any(fnmatch.fnmatchcase(path, i) for i in IGNORE_SYMLINK): continue if os.path.islink(path) and not os.path.exists(path): ok = False error("broken link: %s", path) return ok def error(fmt, *args): sys.stderr.write(output.red(fmt % args)) sys.stderr.write("\n") def main(): ok = True check_funcs = { "libs": check_libs, "usr": check_usr, "shebang": check_shebang, "symlink": check_symlink, } if not sys.stderr.isatty(): output.nocolor() checks = sys.argv[1:] if not checks: checks = check_funcs.keys() for check in checks: func = check_funcs.get(check) if func: ok = func() and ok else: error("Unknown test name '%s'", check) error("Valid tests: %s", " ".join(check_funcs)) ok = False return 0 if ok else 1 if __name__ == "__main__": sys.exit(main())