summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorandroid-build-team Robot <android-build-team-robot@google.com>2020-02-21 03:09:33 +0000
committerandroid-build-team Robot <android-build-team-robot@google.com>2020-02-21 03:09:33 +0000
commitf0aaa21242e4e32cf67b267f9e0f29178e5095a0 (patch)
tree087d2b3f2ca7530daffab5d4bc5a196aba4077e1
parent787d5745ec732182376c0076985367ffd15adc42 (diff)
parentb95080ba9c72ea228d90023aa646cb4df25cf84c (diff)
downloadbuild-android-vts-11.0_r1.tar.gz
Snap for 6224475 from b95080ba9c72ea228d90023aa646cb4df25cf84c to rvc-releaseandroid-vts-11.0_r9android-vts-11.0_r8android-vts-11.0_r7android-vts-11.0_r6android-vts-11.0_r5android-vts-11.0_r4android-vts-11.0_r3android-vts-11.0_r2android-vts-11.0_r16android-vts-11.0_r15android-vts-11.0_r14android-vts-11.0_r13android-vts-11.0_r12android-vts-11.0_r11android-vts-11.0_r10android-vts-11.0_r1android-security-11.0.0_r76android-security-11.0.0_r75android-security-11.0.0_r74android-security-11.0.0_r73android-security-11.0.0_r72android-security-11.0.0_r71android-security-11.0.0_r70android-security-11.0.0_r69android-security-11.0.0_r68android-security-11.0.0_r67android-security-11.0.0_r66android-security-11.0.0_r65android-security-11.0.0_r64android-security-11.0.0_r63android-security-11.0.0_r62android-security-11.0.0_r61android-security-11.0.0_r60android-security-11.0.0_r59android-security-11.0.0_r58android-security-11.0.0_r57android-security-11.0.0_r56android-security-11.0.0_r55android-security-11.0.0_r54android-security-11.0.0_r53android-security-11.0.0_r52android-security-11.0.0_r51android-security-11.0.0_r50android-security-11.0.0_r49android-security-11.0.0_r1android-platform-11.0.0_r5android-platform-11.0.0_r4android-platform-11.0.0_r2android-platform-11.0.0_r1android-cts-11.0_r9android-cts-11.0_r8android-cts-11.0_r7android-cts-11.0_r6android-cts-11.0_r5android-cts-11.0_r4android-cts-11.0_r3android-cts-11.0_r2android-cts-11.0_r16android-cts-11.0_r15android-cts-11.0_r14android-cts-11.0_r13android-cts-11.0_r12android-cts-11.0_r11android-cts-11.0_r10android-cts-11.0_r1android-11.0.0_r6android-11.0.0_r5android-11.0.0_r4android-11.0.0_r3android-11.0.0_r25android-11.0.0_r2android-11.0.0_r17android-11.0.0_r1android11-tests-releaseandroid11-security-releaseandroid11-s1-releaseandroid11-releaseandroid11-platform-releaseandroid11-gsi
Change-Id: I991a58bc99d73b5c9d439c50cb7105926a2adc5a
-rwxr-xr-xabi/kmi_defines.py772
1 files changed, 772 insertions, 0 deletions
diff --git a/abi/kmi_defines.py b/abi/kmi_defines.py
new file mode 100755
index 0000000..a1e45a7
--- /dev/null
+++ b/abi/kmi_defines.py
@@ -0,0 +1,772 @@
+#!/usr/bin/env python3
+"""kmi_defines extract #define compile time constants from a Linux build.
+
+The kmi_defines tool is used to examine the output of a Linux build
+and extract from it C #define statements that define compile time
+constant expressions for the purpose of tracking them as part of the
+KMI (Kernel Module Interface) so that changes to their values can be
+prevented so as to ensure a constant KMI for kernel modules for the
+AOSP GKI Linux kernel project.
+
+This code is python3 only, it does not require any from __future__
+imports. This is a standalone program, it is not meant to be used as
+a module by other programs.
+
+This program runs under the multiprocessing module. Work done within
+a multiprocessing.Pool does not perform error logging or affects any
+state other than the value that it computes and returns via the function
+mapped through the pool's map() function. The reason that no external
+state is affected (for example error loggiing) is to avoid to have to
+even think about what concurrent updates would cause to shuch a facility.
+"""
+
+# TODO(pantin): per Matthias review feedback: "drop the .py from the
+# filename after(!) the review has completed. As last action. Until
+# then we can have the syntax highlighting here in Gerrit."
+
+import argparse
+import collections
+import logging
+import multiprocessing
+import os
+import pathlib
+import re
+import subprocess
+import sys
+from typing import List, Optional, Tuple
+from typing import Set # pytype needs this, pylint: disable=unused-import
+
+INDENT = 4 # number of spaces to indent for each depth level
+COMPILER = "clang" # TODO(pantin): should be determined at run-time
+
+# Dependency that is hidden by the transformation of the .o.d file into
+# the .o.cmd file as part of the Linux build environment. This header is
+# purposely removed and replaced by fictitious set of empty header files
+# that were never part of the actual compilation of the .o files. Those
+# fictitious empty files are generated under the build environment output
+# directory in this subdirectory:
+# include/config
+#
+# This is the actual header file that was part of the compilation of every
+# .o file, the HIDDEN_DEP are added to the dependencies of every .o file.
+#
+# It is important that this file be added because it is unknowable whether
+# the #defines in it were depended upon by a module to alter its behaviour
+# at compile time. For example to pass some flags or not pass some flags
+# to a function.
+
+HIDDEN_DEP = "include/generated/autoconf.h"
+
+
+class StopError(Exception):
+ """Exception raised to stop work when an unexpected error occurs."""
+
+
+def dump(this) -> None:
+ """Dump the data in this.
+
+ This is for debugging purposes, it does not handle every type, only
+ the types used by the underlying code are handled. This will not be
+ part of the final code, or if it is, it will be significantly enhanced
+ or replaced by some other introspection mechanism to serialize data.
+ """
+ def dump_this(this, name: str, depth: int) -> None:
+ """Dump the data in this."""
+ if name:
+ name += " = "
+ if isinstance(this, str):
+ indent = " " * (depth * INDENT)
+ print(indent + name + this)
+ elif isinstance(this, bool):
+ indent = " " * (depth * INDENT)
+ print(indent + name + str(this))
+ elif isinstance(this, List):
+ dump_list(this, name, depth)
+ elif isinstance(this, Set):
+ dump_set(this, name, depth)
+ else:
+ dump_object(this, name, depth)
+
+ def dump_list(lst: List[str], name: str, depth: int) -> None:
+ """Dump the data in lst."""
+ indent = " " * (depth * INDENT)
+ print(indent + name + "{")
+ index = 0
+ for entry in lst:
+ dump_this(entry, f"[{index}]", depth + 1)
+ index += 1
+ print(indent + "}")
+
+ def dump_set(aset: Set[str], name: str, depth: int) -> None:
+ """Dump the data in aset."""
+ lst = list(aset)
+ lst.sort()
+ dump_list(lst, name, depth)
+
+ def dump_object(this, name: str, depth: int) -> None:
+ """Dump the data in this."""
+ indent = " " * (depth * INDENT)
+ print(indent + name +
+ re.sub(r"(^<class '__main__\.|'>$)", "", str(type(this))) + " {")
+ for key, val in this.__dict__.items():
+ dump_this(val, key, depth + 1)
+ print(indent + "}")
+
+ dump_this(this, "", 0)
+
+
+def readfile(name: str) -> str:
+ """Open a file and return its contents in a string as its value."""
+ try:
+ with open(name) as file:
+ return file.read()
+ except OSError as os_error:
+ raise StopError("readfile() failed for: " + name + "\n"
+ "original OSError: " + str(os_error.args))
+
+
+def file_must_exist(file: str) -> None:
+ """If file is invalid print raise a StopError."""
+ if not os.path.exists(file):
+ raise StopError("file does not exist: " + file)
+ if not os.path.isfile(file):
+ raise StopError("file is not a regular file: " + file)
+
+
+def makefile_depends_get_dependencies(depends: str) -> List[str]:
+ """Return list with the dependencies of a makefile target.
+
+ Split the makefile depends specification, the name of the dependent is
+ followed by ":" its dependencies follow the ":". There could be spaces
+ around the ":". Line continuation characters, i.e. "\" are consumed by
+ the regular expression that splits the specification.
+
+ This results in a list with the dependent first, and its dependencies
+ in the remainder of the list, return everything in the list other than
+ the first element.
+ """
+ return re.split(r"[:\s\\]+", re.sub(r"[\s\\]*\Z", "", depends))[1:]
+
+
+def makefile_assignment_split(assignment: str) -> Tuple[str, str]:
+ """Split left:=right into a tuple with the left and right parts.
+
+ Spaces around the := are also removed.
+ """
+ result = re.split(r"\s*:=\s*", assignment, maxsplit=1)
+ if len(result) != 2:
+ raise StopError(
+ "expected: 'left<optional_spaces>:=<optional_spaces>right' in: " +
+ assignment)
+ return result[0], result[1] # left, right
+
+
+def get_src_ccline_deps(obj: str) -> Optional[Tuple[str, str, List[str]]]:
+ """Get the C source file, its cc_line, and non C source dependencies.
+
+ If the tool used to produce the object is not the compiler, or if the
+ source file is not a C source file None is returned.
+
+ Otherwise it returns a triplet with the C source file name, its cc_line,
+ the remaining dependencies.
+ """
+ o_cmd = os.path.join(os.path.dirname(obj),
+ "." + os.path.basename(obj) + ".cmd")
+
+ contents = readfile(o_cmd)
+ contents = re.sub(r"\$\(wildcard[^)]*\)", " ", contents)
+ contents = re.sub(r"[ \t]*\\\n[ \t]*", " ", contents)
+ lines = lines_to_list(contents)
+
+ cc_line = None
+ deps = None
+ source = None
+ for line in lines:
+ if line.startswith("cmd_"):
+ cc_line = line
+ elif line.startswith("deps_"):
+ deps = line
+ elif line.startswith("source_"):
+ source = line
+
+ if cc_line is None:
+ raise StopError("missing cmd_* variable in: " + o_cmd)
+ _, cc_line = makefile_assignment_split(cc_line)
+ if cc_line.split(maxsplit=1)[0] != COMPILER:
+ # The object file was made by strip, symbol renames, etc.
+ # i.e. it was not the result of running the compiler, thus
+ # it can not contribute to #define compile time constants.
+ return None
+
+ if source is None:
+ raise StopError("missing source_* variable in: " + o_cmd)
+ _, source = makefile_assignment_split(source)
+ source = source.strip()
+ if not source.endswith(".c"):
+ return None
+
+ if deps is None:
+ raise StopError("missing deps_* variable in: " + o_cmd)
+ _, deps = makefile_assignment_split(deps)
+ dependendencies = deps.split()
+ dependendencies.append(HIDDEN_DEP)
+
+ return source, cc_line, dependendencies
+
+
+def lines_to_list(lines: str) -> List[str]:
+ """Split a string into a list of non-empty lines."""
+ return [line for line in lines.strip().splitlines() if line]
+
+
+def lines_get_first_line(lines: str) -> str:
+ """Return the first non-empty line in lines."""
+ return lines.strip().splitlines()[0]
+
+
+def shell_line_to_o_files_list(line: str) -> List[str]:
+ """Return a list of .o files in the files list."""
+ return [entry for entry in line.split() if entry.endswith(".o")]
+
+
+def run(args: List[str],
+ raise_on_failure: bool = True) -> subprocess.CompletedProcess:
+ """Run the program specified in args[0] with the arguments in args[1:]."""
+ try:
+ # This argument does not always work for subprocess.run() below:
+ # check=False
+ # neither that nor:
+ # check=True
+ # prevents an exception from being raised if the program that
+ # will be executed is not found
+
+ completion = subprocess.run(args, capture_output=True, text=True)
+ if completion.returncode != 0 and raise_on_failure:
+ raise StopError("execution failed for: " + " ".join(args))
+ return completion
+ except OSError as os_error:
+ raise StopError("failure executing: " + " ".join(args) + "\n"
+ "original OSError: " + str(os_error.args))
+
+
+class KernelModule:
+ """A kernel module, i.e. a *.ko file."""
+ def __init__(self, kofile: str) -> None:
+ """Construct a KernelModule object."""
+ # An example argument is used below, assuming kofile is:
+ # possibly/empty/dirs/modname.ko
+ #
+ # Meant to refer to this module, shown here relative to the top of
+ # the build directory:
+ # drivers/usb/gadget/udc/modname.ko
+ # the values assigned to the members are shown in the comments below.
+
+ self._file = os.path.realpath(kofile) # /abs/dirs/modname.ko
+ self._base = os.path.basename(self._file) # modname.ko
+ self._directory = os.path.dirname(self._file) # /abs/dirs
+ self._cmd_file = os.path.join(self._directory,
+ "." + self._base + ".cmd")
+ self._cmd_text = readfile(self._cmd_file)
+
+ # Some builds append a '; true' to the .modname.ko.cmd, remove it
+
+ self._cmd_text = re.sub(r";\s*true\s*$", "", self._cmd_text)
+
+ # The modules .modname.ko.cmd file contains a makefile snippet,
+ # for example:
+ # cmd_drivers/usb/gadget/udc/dummy_hcd.ko := ld.lld -r ...
+ #
+ # Split the string prior to the spaces followed by ":=", and get
+ # the first element of the resulting list. If the string was not
+ # split (because it did not contain a ":=" then the input string
+ # is returned, by the re.sub() below, as the only element of the list.
+
+ left, _ = makefile_assignment_split(self._cmd_text)
+ self._rel_file = re.sub(r"^cmd_", "", left)
+ if self._rel_file == left:
+ raise StopError("expected: 'cmd_' at start of content of: " +
+ self._cmd_file)
+
+ base = os.path.basename(self._rel_file)
+ if base != self._base:
+ raise StopError("module name mismatch: " + base + " vs " +
+ self._base)
+
+ self._rel_dir = os.path.dirname(self._rel_file)
+
+ # The final step in the build of kernel modules is based on two .o
+ # files, one with the module name followed by .o and another followed
+ # by .mod.o
+ #
+ # The following test verifies that assumption, in case a module is
+ # built differently in the future.
+ #
+ # Even when there are multiple source files, the .o files that result
+ # from compiling them are all linked into a single .o file through an
+ # intermediate link step, that .o files is named:
+ # os.path.join(self._rel_dir, kofile_name + ".o")
+
+ kofile_name, _ = os.path.splitext(self._base)
+ objs = shell_line_to_o_files_list(self._cmd_text)
+ objs.sort()
+ expected = [ # sorted, i.e.: .mod.o < .o
+ os.path.join(self._rel_dir, kofile_name + ".mod.o"),
+ os.path.join(self._rel_dir, kofile_name + ".o")
+ ]
+ if objs != expected:
+ raise StopError("unexpected .o files in: " + self._cmd_file)
+
+ def get_build_dir(self) -> str:
+ """Return the top level build directory.
+
+ I.e. the directory where the output of the Linux build is stored.
+
+ Note that this, like pretty much all the code, can raise an exception,
+ by construction, if an exception is raised while an object is being
+ constructed, or after it is constructed, the object will not be used
+ thereafter (at least not any object explicitly created by this
+ program). Many other places, for example the ones that call readfile()
+ can raise exceptions, the code is located where it belongs.
+
+ In this specific case, the computation of index, and the derived
+ invariant that it be >= 0, is predicated by the condition checked
+ below, if the exception is not raised, then index is >= 0.
+ """
+ if not self._file.endswith(self._rel_file):
+ raise StopError("could not find: " + self._rel_file +
+ " at end of: " + self._file)
+ index = len(self._file) - len(self._rel_file)
+ if index > 0 and self._file[index - 1] == os.sep:
+ index -= 1
+ build_dir = self._file[0:index]
+ return build_dir
+
+ def get_object_files(self, build_dir: str) -> List[str]:
+ """Return a list object files that used to link the kernel module.
+
+ The ocmd_file is the file with extension ".o.cmd" (see below).
+ If the ocmd_file has a more than one line in it, its because the
+ module is made of a single source file and the ocmd_file has the
+ compilation rule and dependencies to build it. If it has a single
+ line single line it is because it builds the .o file by linking
+ multiple .o files.
+ """
+
+ kofile_name, _ = os.path.splitext(self._base)
+ ocmd_file = os.path.join(build_dir, self._rel_dir,
+ "." + kofile_name + ".o.cmd")
+ ocmd_content = readfile(ocmd_file)
+
+ olines = lines_to_list(ocmd_content)
+ if len(olines) > 1: # module made from a single .o file
+ return [os.path.join(build_dir, self._rel_dir, kofile_name + ".o")]
+
+ # Multiple .o files in the module
+
+ _, ldline = makefile_assignment_split(olines[0])
+ return [
+ os.path.realpath(os.path.join(build_dir, obj))
+ for obj in shell_line_to_o_files_list(ldline)
+ ]
+
+
+class Kernel:
+ """The Linux kernel component itself, i.e. vmlinux.o."""
+ def __init__(self, kernel: str) -> None:
+ """Construct a Kernel object."""
+ self._kernel = os.path.realpath(kernel)
+ self._build_dir = os.path.dirname(self._kernel)
+ libs = os.path.join(self._build_dir, "vmlinux.libs")
+ objs = os.path.join(self._build_dir, "vmlinux.objs")
+ file_must_exist(libs)
+ file_must_exist(objs)
+ contents = readfile(libs)
+ archives_and_objects = contents.split()
+ contents = readfile(objs)
+ archives_and_objects += contents.split()
+ self._archives_and_objects = [(os.path.join(self._build_dir, file)
+ if not os.path.isabs(file) else file)
+ for file in archives_and_objects]
+
+ def get_build_dir(self) -> str:
+ """Return the top level build directory.
+
+ I.e. the directory where the output of the Linux build is stored.
+ """
+ return self._build_dir
+
+ def get_object_files(self, build_dir: str) -> List[str]:
+ """Return a list object files that where used to link the kernel."""
+ olist = []
+ for file in self._archives_and_objects:
+ if file.endswith(".o"):
+ if not os.path.isabs(file):
+ file = os.path.join(build_dir, file)
+ olist.append(os.path.realpath(file))
+ continue
+
+ if not file.endswith(".a"):
+ raise StopError("unknown file type: " + file)
+
+ completion = run(["ar", "t", file])
+ objs = lines_to_list(completion.stdout)
+
+ for obj in objs:
+ if not os.path.isabs(obj):
+ obj = os.path.join(build_dir, obj)
+ olist.append(os.path.realpath(obj))
+
+ return olist
+
+
+class Target: # pylint: disable=too-few-public-methods
+ """Target of build and the information used to build it."""
+
+ # The compiler invocation has this form:
+ # clang -Wp,-MD,file.o.d ... -c -o file.o file.c
+ # these constants reflect that knowledge in the code, e.g.:
+ # - the "-Wp,_MD,file.o.d" is at WP_MD_FLAG_INDEX
+ # - the "-c" is at index C_FLAG_INDEX
+ # - the "-o" is at index O_FLAG_INDEX
+ # - the "file.o" is at index OBJ_INDEX
+ # - the "file.c" is at index SRC_INDEX
+ #
+ # There must be at least MIN_CC_LIST_LEN options in that command line.
+ # This knowledge is verified at run time in __init__(), see comments
+ # there.
+
+ MIN_CC_LIST_LEN = 6
+ WP_MD_FLAG_INDEX = 1
+ C_FLAG_INDEX = -4
+ O_FLAG_INDEX = -3
+ OBJ_INDEX = -2
+ SRC_INDEX = -1
+
+ def __init__(self, obj: str, src: str, cc_line: str,
+ deps: List[str]) -> None:
+ self._obj = obj
+ self._src = src
+ self._deps = deps
+
+ # The cc_line, eventually slightly modified, will be used to run
+ # the compiler in various ways. The cc_line could be fed through
+ # the shell to deal with the single-quotes in the cc_line that are
+ # there to quote the double-quotes meant to be part of a C string
+ # literal. Specifically, this occurs in to pass KBUILD_MODNAME and
+ # KBUILD_BASENAME, for example:
+ # -DKBUILD_MODNAME='"aes_ce_cipher"'
+ # -DKBUILD_BASENAME='"aes_cipher_glue"'
+ #
+ # Causing an extra execve(2) of the shell, just to deal with a few
+ # quotes is wasteful, so instead, here the quotes, in this specific
+ # case are removed. This can be done, easiest just by removing the
+ # single quotes with:
+ # cc_cmd = re.sub(r"'", "", cc_line)
+ #
+ # But this could mess up other quote usage in the future, for example
+ # using double quotes or backslash to quote a single quote meant to
+ # actually be seen by the compiler.
+ #
+ # As an alternative, and for this to be more robust, the specific
+ # cases that are known, i.e. the two -D shown above, are dealt with
+ # individually and if there are any single or double quotes, or
+ # backslashes the underlying work is stopped.
+ #
+ # Note that the cc_line comes from the .foo.o.cmd file which is a
+ # makefile snippet, so the actual syntax there is also subject to
+ # whatever other things make would want to do with them. Instead
+ # of doing the absolutely correct thing, which would actually be
+ # to run this through make to have make run then through the shell
+ # this program already has knowledge about these .cmd files and how
+ # they are formed. This compromise, or coupling of knowledge, is a
+ # source of fragility, but not expected to cause much trouble in the
+ # future as the Linux build evolves.
+
+ cc_cmd = re.sub(
+ r"""-D(KBUILD_BASENAME|KBUILD_MODNAME)='("[a-zA-Z0-9_.:]*")'""",
+ r"-D\1=\2", cc_line)
+ cc_list = cc_cmd.split()
+
+ # TODO(pantin): the handling of -D... arguments above is done better
+ # in a later commit by using shlex.split(). Please ignore for now.
+ # TODO(pantin): possibly use ArgumentParser to make this more robust.
+
+ # The compiler invocation has this form:
+ # clang -Wp,-MD,file.o.d ... -c -o file.o file.c
+ #
+ # The following checks are here to ensure that if this assumption is
+ # broken, failures occur. The indexes *_INDEX are hardcoded, they
+ # could in principle be determined at run time, the -o argument could
+ # be in a future update to the Linux build could changed to be a
+ # single argument with the object file name (as in: -ofile.o) which
+ # could also be detected in code at a later time.
+
+ if (len(cc_list) < Target.MIN_CC_LIST_LEN
+ or not cc_list[Target.WP_MD_FLAG_INDEX].startswith("-Wp,-MD,")
+ or cc_list[Target.C_FLAG_INDEX] != "-c"
+ or cc_list[Target.O_FLAG_INDEX] != "-o"):
+ raise StopError("unexpected or missing arguments for: " + obj +
+ " cc_line: " + cc_line)
+
+ # Instead of blindly normalizing the source and object arguments,
+ # they are only normalized if that allows the expected invariants
+ # to be verified, otherwise they are left undisturbed. Note that
+ # os.path.normpath() does not turn relative paths into absolute
+ # paths, it just removes up-down walks (e.g. a/b/../c -> a/c).
+
+ def verify_file(file: str, index: int, kind: str, cc_list: List[str],
+ target_file: str) -> None:
+ file_in_cc_list = cc_list[index]
+ if not file.endswith(file_in_cc_list):
+ file_normalized = os.path.normpath(file_in_cc_list)
+ if not file.endswith(file_normalized):
+ raise StopError(f"unexpected {kind} argument for: "
+ f"{target_file} value was: "
+ f"{file_in_cc_list}")
+ cc_list[index] = file_normalized
+
+ verify_file(obj, Target.OBJ_INDEX, "object", cc_list, obj)
+ verify_file(src, Target.SRC_INDEX, "source", cc_list, obj)
+
+ self._cc_list = cc_list
+
+
+class KernelComponentBase: # pylint: disable=too-few-public-methods
+ """Base class for KernelComponentCreationError and KernelComponent.
+
+ There is not much purpose for this class other than to satisfy the strong
+ typing checks of pytype, with looser typing, this could be removed but at
+ the risk of invoking member functions at run-time on objects that do not
+ provide them. Having this class makes the code more reliable.
+ """
+ def get_error(self) -> Optional[str]: # pylint: disable=no-self-use
+ """Return None for the error, means there was no error."""
+ return None
+
+ def get_deps_set(self) -> Set[str]: # pylint: disable=no-self-use
+ """Return the set of dependencies for the kernel component."""
+ return set()
+
+ def is_kernel(self) -> bool: # pylint: disable=no-self-use
+ """Is this the kernel?"""
+ return False
+
+
+class KernelComponentCreationError(KernelComponentBase): # pylint: disable=too-few-public-methods
+ """A KernelComponent creation error.
+
+ When a KernelComponent creation fails, or the creation of its subordinate
+ Kernel or KernelModule creation fails, a KernelComponentCreationError
+ object is created to store the information relevant to the failure.
+ """
+ def __init__(self, filename: str, error: str) -> None:
+ """Construct a KernelComponentCreationError object."""
+ self._error = error
+ self._filename = filename
+
+ def get_error(self) -> Optional[str]:
+ """Return the error."""
+ return self._filename + ": " + self._error
+
+
+class KernelComponent(KernelComponentBase):
+ """A kernel component, either vmlinux.o or a *.ko file.
+
+ Inspect a Linux kernel module (a *.ko file) or the Linux kernel to
+ determine what was used to build it: object filess, source files, header
+ files, and other information that is produced as a by-product of its build.
+ """
+ def __init__(self, filename: str) -> None:
+ """Construct a KernelComponent object."""
+ if filename.endswith("vmlinux.o"):
+ self._kernel = True
+ self._kind = Kernel(filename)
+ else:
+ self._kernel = False
+ self._kind = KernelModule(filename)
+ self._build_dir = self._kind.get_build_dir()
+ self._source_dir = self._get_source_dir()
+ self._files_o = self._kind.get_object_files(self._build_dir)
+ self._files_o.sort()
+
+ # using a set because there is no unique flag to list.sort()
+ deps_set = set()
+
+ self._targets = []
+ for obj in self._files_o:
+ file_must_exist(obj)
+ result = get_src_ccline_deps(obj)
+ if result is None:
+ continue
+ src, cc_line, dependendencies = result
+
+ file_must_exist(src)
+ depends = []
+ for dep in dependendencies:
+ if not os.path.isabs(dep):
+ dep = os.path.join(self._build_dir, dep)
+ dep = os.path.realpath(dep)
+ depends.append(dep)
+ deps_set.add(dep)
+
+ if not os.path.isabs(src):
+ src = os.path.join(self._build_dir, src)
+ src = os.path.realpath(src)
+ self._targets.append(Target(obj, src, cc_line, depends))
+
+ for dep in [dep for dep in list(deps_set) if not dep.endswith(".h")]:
+ deps_set.remove(dep)
+ self._deps_set = deps_set
+
+ def _get_source_dir(self) -> str:
+ """Return the top level Linux kernel source directory."""
+ source = os.path.join(self._build_dir, "source")
+ if not os.path.islink(source):
+ raise StopError("could not find source symlink: " + source)
+
+ if not os.path.isdir(source):
+ raise StopError("source symlink not a directory: " + source)
+
+ source_dir = os.path.realpath(source)
+ if not os.path.isdir(source_dir):
+ raise StopError("source directory not a directory: " + source_dir)
+
+ return source_dir
+
+ def get_deps_set(self) -> Set[str]:
+ """Return the set of dependencies for the kernel component."""
+ return self._deps_set
+
+ def is_kernel(self) -> bool:
+ """Is this the kernel?"""
+ return self._kernel
+
+
+def kernel_component_factory(filename: str) -> KernelComponentBase:
+ """Make an InfoKmod or an InfoKernel object for file and return it."""
+ try:
+ return KernelComponent(filename)
+ except StopError as stop_error:
+ return KernelComponentCreationError(filename,
+ " ".join([*stop_error.args]))
+
+
+class KernelComponentProcess(multiprocessing.Process):
+ """Process to make the KernelComponent concurrently."""
+ def __init__(self) -> None:
+ multiprocessing.Process.__init__(self)
+ self._queue = multiprocessing.Queue()
+ self.start()
+
+ def run(self) -> None:
+ """Create and save the KernelComponent."""
+ self._queue.put(kernel_component_factory("vmlinux.o"))
+
+ def get_component(self) -> KernelComponentBase:
+ """Return the kernel component."""
+ kernel_component = self._queue.get()
+ self.join() # must be after queue.get() otherwise it deadlocks
+ return kernel_component
+
+
+def work_on_all_components(options) -> List[KernelComponentBase]:
+ """Return a list of KernelComponentBase objects."""
+ files = [str(ko) for ko in pathlib.Path().rglob("*.ko")]
+ if options.sequential:
+ return [
+ kernel_component_factory(file) for file in ["vmlinux.o"] + files
+ ]
+
+ # There is significantly more work to be done for the vmlinux.o than
+ # the *.ko kernel modules. A dedicated process is started to do the
+ # work for vmlinux.o as soon as possible instead of leaving it to the
+ # vagaries of multiprocessing.Pool() and how it would spreads the work.
+ # This significantly reduces the elapsed time for this work.
+
+ kernel_component_process = KernelComponentProcess()
+
+ chunk_size = 128
+ processes = max(1, len(files) // (chunk_size * 3))
+ processes = min(processes, os.cpu_count())
+ with multiprocessing.Pool(processes) as pool:
+ components = pool.map(kernel_component_factory, files, chunk_size)
+
+ kernel_component = kernel_component_process.get_component()
+
+ return [kernel_component] + components
+
+
+def work_on_whole_build(options) -> int:
+ """Work on the whole build to extract the #define constants."""
+ if not os.path.isfile("vmlinux.o"):
+ logging.error("file not found: vmlinux.o")
+ return 1
+ components = work_on_all_components(options)
+ failed = False
+ header_count = collections.defaultdict(int)
+ for comp in components:
+ error = comp.get_error()
+ if error:
+ logging.error(error)
+ failed = True
+ continue
+ deps_set = comp.get_deps_set()
+ for header in deps_set:
+ header_count[header] += 1
+ if failed:
+ return 1
+ if options.dump:
+ dump(components)
+ if options.dump and options.includes:
+ print()
+ if options.includes:
+ for header, count in header_count.items():
+ if count >= 2:
+ print(header)
+ return 0
+
+
+def main() -> int:
+ """Extract #define compile time constants from a Linux build."""
+ def existing_file(file):
+ if not os.path.isfile(file):
+ raise argparse.ArgumentTypeError(
+ "{0} is not a valid file".format(file))
+ return file
+
+ parser = argparse.ArgumentParser()
+ parser.add_argument("-d",
+ "--dump",
+ action="store_true",
+ help="dump internal state")
+ parser.add_argument("-s",
+ "--sequential",
+ action="store_true",
+ help="execute without concurrency")
+ group = parser.add_mutually_exclusive_group()
+ group.add_argument("-i",
+ "--includes",
+ action="store_true",
+ help="show relevant include files")
+ group.add_argument("-c",
+ "--component",
+ type=existing_file,
+ help="show information for a component")
+ options = parser.parse_args()
+
+ if not options.component:
+ return work_on_whole_build(options)
+
+ comp = kernel_component_factory(options.component)
+
+ error = comp.get_error()
+ if error:
+ logging.error(error)
+ return 1
+ if options.dump:
+ dump([comp])
+ return 0
+
+
+if __name__ == "__main__":
+ sys.exit(main())