Merge changes Ice859e8e,I44915dcd
* changes:
Add typing info and documentation to ndk.builds.
Provide type annotations for ndk.paths.
diff --git a/ndk/abis.py b/ndk/abis.py
index 91bb998..76f36f1 100644
--- a/ndk/abis.py
+++ b/ndk/abis.py
@@ -14,6 +14,10 @@
# limitations under the License.
#
"""Constants and helper functions for NDK ABIs."""
+from typing import NewType
+
+
+Arch = NewType('Arch', str)
LP32_ABIS = (
@@ -32,10 +36,10 @@
ALL_ARCHITECTURES = (
- 'arm',
- 'arm64',
- 'x86',
- 'x86_64',
+ Arch('arm'),
+ Arch('arm64'),
+ Arch('x86'),
+ Arch('x86_64'),
)
diff --git a/ndk/builds.py b/ndk/builds.py
index c3d6b9b..a2c7cc6 100644
--- a/ndk/builds.py
+++ b/ndk/builds.py
@@ -19,12 +19,13 @@
"""
from __future__ import absolute_import
+from enum import auto, Enum, unique
import ntpath
import os
import shutil
import stat
import subprocess
-from typing import Iterable, Set
+from typing import Iterable, List, Optional, Set
import ndk.abis
import ndk.ext.shutil
@@ -33,21 +34,27 @@
class ModuleValidateError(RuntimeError):
+ """The error raised when module validation fails."""
pass
-class NoticeGroup:
+@unique
+class NoticeGroup(Enum):
"""An enum describing NOTICE file groupings.
The NDK ships two NOTICE files: one for the toolchain, and one for
everything else.
"""
- BASE = 1
- TOOLCHAIN = 2
+ BASE = auto()
+ TOOLCHAIN = auto()
class BuildContext:
- def __init__(self, out_dir, dist_dir, modules, host, arches, build_number):
+ """Class containing build context information."""
+
+ def __init__(self, out_dir: str, dist_dir: str, modules: List['Module'],
+ host: ndk.hosts.Host, arches: List[ndk.abis.Arch],
+ build_number: str) -> None:
self.out_dir = out_dir
self.dist_dir = dist_dir
self.modules = {m.name: m for m in modules}
@@ -57,6 +64,8 @@
class Module:
+ """Base module type for the build system."""
+
name: str
path: str
deps: Set[str] = set()
@@ -69,7 +78,7 @@
# interface is a single path, not a list. For the rare modules that have
# multiple notice files (such as yasm), the notices property should be
# overrided. By default this property will return `[self.notice]`.
- notice = None
+ notice: Optional[str] = None
# Not all components need a notice (stub scripts, basic things like the
# readme and changelog, etc), but this is opt-out.
@@ -95,29 +104,48 @@
self.validate()
@property
- def notices(self):
+ def notices(self) -> List[str]:
+ """Returns the list of notice files for this module."""
if self.no_notice:
return []
if self.notice is None:
return []
return [self.notice]
- def default_notice_path(self): # pylint: disable=no-self-use
+ def default_notice_path(self) -> Optional[str]:
+ """Returns the path to the default notice for this module, if any."""
return None
- def validate_error(self, msg):
- return ModuleValidateError('{}: {}'.format(self.name, msg))
+ def validate_error(self, msg: str) -> ModuleValidateError:
+ """Creates a validation error for this module.
- def validate(self):
+ Automatically includes the module name in the error string.
+
+ Args:
+ msg: Detailed error message.
+ """
+ return ModuleValidateError(f'{self.name}: {msg}')
+
+ def validate(self) -> None:
+ """Validates module config.
+
+ Raises:
+ ModuleValidateError: The module configuration is not valid.
+ """
if self.name is None:
- raise ModuleValidateError('{} has no name'.format(self.__class__))
+ raise ModuleValidateError(f'{self.__class__} has no name')
if self.path is None:
raise self.validate_error('path property not set')
- if self.notice_group not in (NoticeGroup.BASE, NoticeGroup.TOOLCHAIN):
+ if self.notice_group not in NoticeGroup:
raise self.validate_error('invalid notice group')
self.validate_notice()
- def validate_notice(self):
+ def validate_notice(self) -> None:
+ """Validates the notice files of this module.
+
+ Raises:
+ ModuleValidateError: The module configuration is not valid.
+ """
if self.no_notice:
return
@@ -126,36 +154,76 @@
for notice in self.notices:
if not os.path.exists(notice):
raise self.validate_error(
- 'notice file {} does not exist'.format(notice))
+ f'notice file {notice} does not exist')
- def get_dep(self, name):
+ def get_dep(self, name: str) -> 'Module':
+ """Returns the module object for the given dependency.
+
+ Returns:
+ The module object for the given dependency.
+
+ Raises:
+ KeyError: The given name does not match any of this module's
+ dependencies.
+ """
if name not in self.deps:
raise KeyError
return self.context.modules[name]
- def get_build_host_install(self, arch=None):
+ def get_build_host_install(self,
+ arch: Optional[ndk.abis.Arch] = None) -> str:
+ """Returns the module's install path for the current host.
+
+ In a cross-compiling context (i.e. building the Windows NDK from
+ Linux), this will return the install directory for the build OS rather
+ than the target OS.
+
+ Args:
+ arch: Architecture to fetch for architecture-specific modules.
+
+ Returns:
+ This module's install path for the build host.
+ """
return self.get_install_path(ndk.hosts.get_default_host(), arch)
@property
- def out_dir(self):
+ def out_dir(self) -> str:
+ """Base out directory for the current build."""
return self.context.out_dir
@property
- def dist_dir(self):
+ def dist_dir(self) -> str:
+ """Base dist directory for the current build."""
return self.context.dist_dir
@property
- def host(self):
+ def host(self) -> ndk.hosts.Host:
+ """Host for the current build."""
return self.context.host
@property
- def arches(self):
+ def arches(self) -> List[ndk.abis.Arch]:
+ """Architectures targeted by the current build."""
return self.context.arches
- def build(self):
+ def build(self) -> None:
+ """Builds the module.
+
+ A module's dependencies are guaranteed to have been installed before
+ its build begins.
+
+ The build phase should not modify the install directory.
+ """
raise NotImplementedError
- def install(self):
+ def install(self) -> None:
+ """Installs the module.
+
+ Install happens after the module has been built.
+
+ The install phase should only copy files, not create them. Compilation
+ should happen in the build phase.
+ """
package_installs = ndk.packaging.expand_packages(
self.name, self.path, self.host, self.arches)
@@ -168,12 +236,34 @@
shutil.rmtree(install_path)
ndk.packaging.extract_zip(package, install_path)
- def get_install_paths(self, host, arches):
+ def get_install_paths(
+ self, host: ndk.hosts.Host,
+ arches: Optional[Iterable[ndk.abis.Arch]]) -> List[str]:
+ """Returns the install paths for the given archiectures."""
install_subdirs = ndk.packaging.expand_paths(self.path, host, arches)
install_base = ndk.paths.get_install_path(self.out_dir, host)
return [os.path.join(install_base, d) for d in install_subdirs]
- def get_install_path(self, host=None, arch=None):
+ def get_install_path(self, host: Optional[ndk.hosts.Host] = None,
+ arch: Optional[ndk.abis.Arch] = None) -> str:
+ """Returns the install path for the given module config.
+
+ For an architecture-independent module, there should only ever be one
+ install path.
+
+ For an architecture-dependent module, the optional arch argument must
+ be provided to select between the install paths.
+
+ Args:
+ host: The host to use for a host-specific install path.
+ arch: The architecture to use for an architecure-dependent module.
+
+ Raises:
+ ValueError: This is an architecture-dependent module and no
+ architecture was provided.
+ RuntimeError: An architecture-independent module has non-unique
+ install paths.
+ """
if host is None:
host = self.host
@@ -194,7 +284,7 @@
arches = [self.build_arch]
elif arch_dependent:
raise ValueError(
- 'get_install_path for {} requires valid arch'.format(arch))
+ f'get_install_path for {arch} requires valid arch')
install_subdirs = self.get_install_paths(host, arches)
@@ -206,7 +296,7 @@
def __str__(self):
if self.split_build_by_arch and self.build_arch is not None:
- return '{} [{}]'.format(self.name, self.build_arch)
+ return f'{self.name} [{self.build_arch}]'
return self.name
def __hash__(self):
@@ -219,26 +309,36 @@
return str(self) == str(other)
@property
- def log_file(self):
+ def log_file(self) -> str:
+ """Returns the basename of the log file for this module."""
if self.split_build_by_arch and self.build_arch is not None:
- return '{}-{}.log'.format(self.name, self.build_arch)
+ return f'{self.name}-{self.build_arch}.log'
elif self.split_build_by_arch:
raise RuntimeError('Called log_file on unsplit module')
else:
- return '{}.log'.format(self.name)
+ return f'{self.name}.log'
- def log_path(self, log_dir):
+ def log_path(self, log_dir: str) -> str:
+ """Returns the path to the log file for this module."""
return os.path.join(log_dir, self.log_file)
class PackageModule(Module):
- src = None
+ """A directory to be installed to the NDK.
+
+ No transformation is performed on the installed directory.
+ """
+
+ #: The absolute path to the directory to be installed.
+ src: str
+
+ #: If true, create a repo.prop file for this module.
create_repo_prop = False
- def default_notice_path(self):
+ def default_notice_path(self) -> str:
return os.path.join(self.src, 'NOTICE')
- def validate(self):
+ def validate(self) -> None:
super().validate()
if ndk.packaging.package_varies_by(self.path, 'abi'):
@@ -254,10 +354,10 @@
raise self.validate_error(
'PackageModule cannot vary by triple')
- def build(self):
+ def build(self) -> None:
pass
- def install(self):
+ def install(self) -> None:
install_paths = self.get_install_paths(self.host,
ndk.abis.ALL_ARCHITECTURES)
assert len(install_paths) == 1
@@ -268,53 +368,78 @@
class InvokeExternalBuildModule(Module):
+ """A module that uses a build.py script.
+
+ These are legacy modules that have not yet been properly merged into
+ checkbuild.py.
+ """
+
+ #: The path to the build script relative to the top of the source tree.
script: str
+
+ #: True if the module can be built in parallel per-architecture.
arch_specific = False
- def build(self):
+ def build(self) -> None:
build_args = common_build_args(self.out_dir, self.dist_dir, self.host)
if self.split_build_by_arch:
- build_args.append('--arch={}'.format(self.build_arch))
+ build_args.append(f'--arch={self.build_arch}')
elif self.arch_specific and len(self.arches) == 1:
- build_args.append('--arch={}'.format(self.arches[0]))
+ build_args.append(f'--arch={self.arches[0]}')
elif self.arches == ndk.abis.ALL_ARCHITECTURES:
pass
else:
raise NotImplementedError(
- 'Module {} can only build all architectures or none'.format(
- self.name))
+ f'Module {self.name} can only build all architectures or none')
script = self.get_script_path()
invoke_external_build(script, build_args)
- def get_script_path(self):
+ def get_script_path(self) -> str:
+ """Returns the absolute path to the build script."""
return ndk.paths.android_path(self.script)
class InvokeBuildModule(InvokeExternalBuildModule):
- def get_script_path(self):
+ """A module that uses a build.py script within ndk/build/tools.
+
+ Identical to InvokeExternalBuildModule, but the script path is relative to
+ ndk/build/tools instead of the top of the source tree.
+ """
+
+ def get_script_path(self) -> str:
return ndk.paths.ndk_path('build/tools', self.script)
class FileModule(Module):
- src = None
+ """A module that installs a single file to the NDK."""
- # Used for things like the readme and the changelog. No notice needed.
+ #: Path to the file to be installed.
+ src: str
+
+ #: True if no notice file is needed for this module.
no_notice = True
- def build(self):
+ def build(self) -> None:
pass
- def install(self):
+ def install(self) -> None:
shutil.copy2(self.src, self.get_install_path())
class MultiFileModule(Module):
+ """A module that installs multiple files to the NDK.
+
+ This is similar to FileModule, but allows multiple files to be installed
+ with a single module.
+ """
+
+ #: List of absolute paths to files to be installed.
files: Iterable[str] = []
- def build(self):
+ def build(self) -> None:
pass
- def install(self):
+ def install(self) -> None:
install_dir = self.get_install_path()
ndk.ext.shutil.create_directory(install_dir)
for file_path in self.files:
@@ -322,13 +447,24 @@
class ScriptShortcutModule(Module):
+ """A module that installs a shortcut to another script in the NDK.
+
+ Some NDK tools are installed to a location other than the top of the NDK
+ (as a result of the modular NDK effort), but we want to make them
+ accessible from the top level because that is where they were Historically
+ installed.
+ """
+
+ #: The path to the installed NDK script, relative to the top of the NDK.
script: str
+
+ #: The file extension for the called script on Windows.
windows_ext: str
# These are all trivial shell scripts that we generated. No notice needed.
no_notice = True
- def validate(self):
+ def validate(self) -> None:
super().validate()
if ndk.packaging.package_varies_by(self.script, 'abi'):
@@ -347,16 +483,17 @@
raise self.validate_error(
'ScriptShortcutModule requires windows_ext')
- def build(self):
+ def build(self) -> None:
pass
- def install(self):
+ def install(self) -> None:
if self.host.startswith('windows'):
self.make_cmd_helper()
else:
self.make_sh_helper()
- def make_cmd_helper(self):
+ def make_cmd_helper(self) -> None:
+ """Makes a .cmd helper script for Windows."""
script = self.get_script_path()
full_path = ntpath.join(
'%~dp0', ntpath.normpath(script) + self.windows_ext)
@@ -368,7 +505,8 @@
full_path + ' %*\n',
])
- def make_sh_helper(self):
+ def make_sh_helper(self) -> None:
+ """Makes a bash helper script for POSIX systems."""
script = self.get_script_path()
full_path = os.path.join('$DIR', script)
@@ -383,7 +521,8 @@
os.chmod(install_path,
mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
- def get_script_path(self):
+ def get_script_path(self) -> str:
+ """Returns the installed path of the script."""
scripts = ndk.packaging.expand_paths(
self.script, self.host, ndk.abis.ALL_ARCHITECTURES)
assert len(scripts) == 1
@@ -391,43 +530,85 @@
class PythonPackage(Module):
- def default_notice_path(self):
+ """A Python package that should be packaged for distribution.
+
+ These are not installed within the NDK, but are packaged (as a source
+ distribution) for archival on the build servers.
+
+ These are used to archive the NDK's build and test tools so test artifacts
+ may be regnenerated using only artifacts from the build server.
+ """
+
+ def default_notice_path(self) -> str:
# Assume there's a NOTICE file in the same directory as the setup.py.
return os.path.join(os.path.dirname(self.path), 'NOTICE')
- def build(self):
+ def build(self) -> None:
cwd = os.path.dirname(self.path)
subprocess.check_call(
['python3', self.path, 'sdist', '-d', self.out_dir], cwd=cwd)
- def install(self):
+ def install(self) -> None:
pass
-def _invoke_build(script, args):
- if args is None:
- args = []
+def invoke_external_build(script: str, args: List[str]) -> None:
+ """Invokes a build.py script rooted within the top level source tree.
+
+ Args:
+ script: Path to the script to be executed within the top level source
+ tree.
+ args: Command line arguments to be passed to the script.
+ """
subprocess.check_call(['python3', ndk.paths.android_path(script)] + args)
-def invoke_build(script, args=None):
- script_path = os.path.join('build/tools', script)
- _invoke_build(ndk.paths.ndk_path(script_path), args)
+def common_build_args(out_dir: str, dist_dir: str,
+ host: ndk.hosts.Host) -> List[str]:
+ """Returns a list of common arguments for build.py scripts.
+ Modules that have not been fully merged into checkbuild.py still use a
+ separately executed build.py script via InvokeBuildModule or
+ InvokeExternalBuildModule. These have a common command line interface for
+ determining out directories and target host.
-def invoke_external_build(script, args=None):
- _invoke_build(ndk.paths.android_path(script), args)
+ Args:
+ out_dir: Base out directory for the target host.
+ dist_dir: Distribution directory for archived artifacts.
+ host: Target host.
-
-def common_build_args(out_dir, dist_dir, host):
+ Returns:
+ List of command line arguments to be used with build.py.
+ """
return [
- '--out-dir={}'.format(os.path.join(out_dir, host)),
- '--dist-dir={}'.format(dist_dir),
- '--host={}'.format(host),
+ f'--out-dir={os.path.join(out_dir, host)}',
+ f'--dist-dir={dist_dir}',
+ f'--host={host}',
]
-def install_directory(src, dst):
+def install_directory(src: str, dst: str) -> None:
+ """Copies a directory to an install location, ignoring some file types.
+
+ The destination will be removed prior to copying if it exists, ensuring a
+ clean install.
+
+ Some file types (currently python intermediates, editor swap files, git
+ directories) will be removed from the install location.
+
+ Args:
+ src: Directory to install.
+ dst: Install location. Will be removed prior to installation. The
+ source directory will be copied *to* this path, not *into* this
+ path.
+ """
+ # TODO: Remove the ignore patterns in favor of purging the install
+ # directory after install, since not everything uses install_directory.
+ #
+ # We already do this to some extent with package_ndk, but we don't cover
+ # all these file types, and we should also do this before packaging since
+ # packaging only runs when a full NDK is built (fine for the build servers,
+ # could potentially be wrong for local testing).
if os.path.exists(dst):
shutil.rmtree(dst)
ignore_patterns = shutil.ignore_patterns(
@@ -435,7 +616,26 @@
shutil.copytree(src, dst, ignore=ignore_patterns)
-def make_repo_prop(out_dir):
+def make_repo_prop(out_dir: str) -> None:
+ """Installs a repro.prop file to the given directory.
+
+ A repo.prop file is a file listing all of the git projects used and their
+ checked out revisions. i.e.
+
+ platform/bionic 40538268d43d82409a93637960f2da3c1226840a
+ platform/development 688f15246399db98897e660889d9a202559fe5d8
+ ...
+
+ Historically we installed one of these per "module" (from the attempted
+ modular NDK), but since the same information can be retrieved from the
+ build number we do not install them for most things now.
+
+ If this build is happening on the build server then there will be a
+ repo.prop file in the DIST_DIR for us to copy, otherwise we generate our
+ own.
+ """
+ # TODO: Finish removing users of this in favor of installing a single
+ # manifest.xml file in the root of the NDK.
file_name = 'repo.prop'
dist_dir = os.environ.get('DIST_DIR')
diff --git a/ndk/hosts.py b/ndk/hosts.py
index 896bfa2..3c853fe 100644
--- a/ndk/hosts.py
+++ b/ndk/hosts.py
@@ -18,13 +18,17 @@
import os
import sys
+from typing import NewType
+
+
+Host = NewType('Host', str)
ALL_HOSTS = (
- 'darwin',
- 'linux',
- 'windows',
- 'windows64',
+ Host('darwin'),
+ Host('linux'),
+ Host('windows'),
+ Host('windows64'),
)
diff --git a/ndk/paths.py b/ndk/paths.py
index 66c9c5e..d763025 100644
--- a/ndk/paths.py
+++ b/ndk/paths.py
@@ -20,25 +20,28 @@
import os
import shutil
import sys
+from typing import Generator, Optional
import ndk.abis
import ndk.config
+import ndk.hosts
THIS_DIR = os.path.realpath(os.path.dirname(__file__))
-def android_path(*args):
+def android_path(*args: str) -> str:
"""Returns the absolute path rooted within the top level source tree."""
return os.path.normpath(os.path.join(THIS_DIR, '../../', *args))
-def ndk_path(*args):
+def ndk_path(*args: str) -> str:
"""Returns the absolute path rooted within the NDK source tree."""
return android_path('ndk', *args)
-def sysroot_path(toolchain):
+
+def sysroot_path(toolchain: str) -> str:
arch = ndk.abis.toolchain_to_arch(toolchain)
# Only ARM has more than one ABI, and they both have the same minimum
# platform level.
@@ -50,11 +53,11 @@
return android_path(prebuilt_ndk, sysroot_subpath)
-def toolchain_path(*args):
+def toolchain_path(*args: str) -> str:
return android_path('toolchain', *args)
-def _get_dir_from_env(default, env_var):
+def _get_dir_from_env(default: str, env_var: str) -> str:
"""Returns the path to a directory specified by the environment.
If the environment variable is not set, the default will be used. The
@@ -73,12 +76,12 @@
return path
-def get_out_dir():
+def get_out_dir() -> str:
"""Returns the out directory."""
return _get_dir_from_env(android_path('out'), 'OUT_DIR')
-def get_dist_dir(out_dir):
+def get_dist_dir(out_dir) -> str:
"""Returns the distribution directory.
The contents of the distribution directory are archived on the build
@@ -87,7 +90,7 @@
return _get_dir_from_env(os.path.join(out_dir, 'dist'), 'DIST_DIR')
-def path_in_out(dirname, out_dir=None):
+def path_in_out(dirname: str, out_dir: Optional[str] = None) -> str:
"""Returns a path within the out directory."
Args:
@@ -104,7 +107,8 @@
return os.path.join(out_dir, dirname)
-def get_install_path(out_dir=None, host=None):
+def get_install_path(out_dir: Optional[str] = None,
+ host: Optional[str] = None) -> str:
"""Returns the built NDK install path.
Note that the path returned might not actually contain the NDK. The NDK may
@@ -129,7 +133,7 @@
@contextlib.contextmanager
-def temp_dir_in_out(dirname, out_dir=None):
+def temp_dir_in_out(dirname: str, out_dir: Optional[str] = None) -> Generator:
"""Creates a well named temporary directory within the out directory.
If the directory exists on context entry, RuntimeError will be raised. The
@@ -159,7 +163,7 @@
shutil.rmtree(abspath)
-def to_posix_path(path):
+def to_posix_path(path: str) -> str:
if sys.platform == 'win32':
return path.replace('\\', '/')
else: