pw presubmit no longer cds.
The pw presubmit command no longer cds to the repository root. By default,
it puts output in repository_root/.presubmit, but output can be placed
anywhere. Both of these presubmit commands pass.
$ cd $PW_ROOT ; pw presubmit
$ cd $PW_ROOT/.. ; pw presubmit --repository ~/pigweed/pigweed \
--output-directory .
The presubmit context object now has an output_directory member. Presubmit
checks should place output in that folder.
Change-Id: Ibb0a3c7add54de6409561006e3c74dd8c8603a25
diff --git a/pw_presubmit/py/pw_presubmit/format_code.py b/pw_presubmit/py/pw_presubmit/format_code.py
index 23c5a94..de1c070 100755
--- a/pw_presubmit/py/pw_presubmit/format_code.py
+++ b/pw_presubmit/py/pw_presubmit/format_code.py
@@ -199,8 +199,13 @@
# Show a copy-and-pastable command to fix the issues.
if show_fix_commands:
- path_relative_to_cwd = (
- lambda p: Path(p).resolve().relative_to(Path.cwd().resolve()))
+
+ def path_relative_to_cwd(path):
+ try:
+ return Path(path).resolve().relative_to(Path.cwd().resolve())
+ except ValueError:
+ return Path(path).resolve()
+
message = (f' pw format --fix {path_relative_to_cwd(path)}'
for path in errors)
_LOG.warning('To fix formatting, run:\n\n%s\n', '\n'.join(message))
diff --git a/pw_presubmit/py/pw_presubmit/pigweed_presubmit.py b/pw_presubmit/py/pw_presubmit/pigweed_presubmit.py
index 7fe86cb..5f78111 100755
--- a/pw_presubmit/py/pw_presubmit/pigweed_presubmit.py
+++ b/pw_presubmit/py/pw_presubmit/pigweed_presubmit.py
@@ -16,9 +16,9 @@
"""Runs the local presubmit checks for the Pigweed repository."""
import argparse
-import glob
import logging
import os
+import pathlib
import re
import shutil
import subprocess
@@ -40,13 +40,6 @@
_LOG: logging.Logger = logging.getLogger(__name__)
-PRESUBMIT_PREFIX = '.presubmit'
-
-
-def presubmit_dir(*paths):
- """Returns a relative path from the presubmit output directory."""
- return os.path.join(PRESUBMIT_PREFIX, *paths)
-
def run_python_module(*args, **kwargs):
return call('python', '-m', *args, **kwargs)
@@ -55,36 +48,38 @@
#
# Initialization
#
-def init_cipd(unused_ctx: PresubmitContext):
- cipd = os.path.abspath('.presubmit/cipd')
- call(sys.executable, 'env_setup/cipd/update.py', '--install-dir', cipd)
+def init_cipd(ctx: PresubmitContext):
+ call(sys.executable,
+ ctx.repository_root.joinpath('env_setup/cipd/update.py'),
+ '--install-dir', ctx.output_directory)
- paths = [cipd, os.path.join(cipd, 'bin')]
- for base in glob.glob(os.path.join(cipd, '*')):
+ paths = [ctx.output_directory, ctx.output_directory.joinpath('bin')]
+ for base in ctx.output_directory.glob('*'):
paths.append(base)
paths.append(os.path.join(base, 'bin'))
paths.append(os.environ['PATH'])
- os.environ['PATH'] = os.pathsep.join(paths)
+ os.environ['PATH'] = os.pathsep.join(str(x) for x in paths)
_LOG.debug('PATH %s', os.environ['PATH'])
-def init_virtualenv(unused_ctx: PresubmitContext):
+def init_virtualenv(ctx: PresubmitContext):
"""Set up virtualenv, assumes recent Python 3 is already installed."""
- venv = os.path.abspath('.presubmit/venv')
+ virtualenv_source = ctx.repository_root.joinpath('env_setup/virtualenv')
# For speed, don't build the venv if it exists. Use --clean to recreate it.
- if not os.path.isdir(venv):
+ if not ctx.output_directory.joinpath('pyvenv.cfg').is_file():
call(
'python3',
- 'env_setup/virtualenv/init.py',
- f'--venv_path={venv}',
- '--requirements=env_setup/virtualenv/requirements.txt',
+ virtualenv_source.joinpath('init.py'),
+ f'--venv_path={ctx.output_directory}',
+ '--requirements={}'.format(
+ virtualenv_source.joinpath('requirements.txt')),
)
os.environ['PATH'] = os.pathsep.join((
- os.path.join(venv, 'bin'),
+ str(ctx.output_directory.joinpath('bin')),
os.environ['PATH'],
))
@@ -102,39 +97,43 @@
return '--args=' + ' '.join(f'{arg}={val}' for arg, val in kwargs.items())
-def gn_gen(*args):
- call('gn', 'gen', '--color=always', '--check', *args)
+def gn_gen(*args, path, repo):
+ call('gn', 'gen', path, '--color=always', '--check', *args, cwd=repo)
-_CLANG_GEN_ARGS = (presubmit_dir('clang'),
- gn_args(
- pw_target_config='"//targets/host/host.gni"',
- pw_target_toolchain='"//pw_toolchain:host_clang_os"'))
+def ninja(path, repo):
+ call('ninja', '-C', path.absolute(), cwd=repo)
-def gn_clang_build(unused_ctx: PresubmitContext):
- gn_gen('--export-compile-commands', *_CLANG_GEN_ARGS)
- call('ninja', '-C', presubmit_dir('clang'))
+_CLANG_GEN_ARGS = gn_args(pw_target_config='"//targets/host/host.gni"',
+ pw_target_toolchain='"//pw_toolchain:host_clang_os"')
+
+
+def gn_clang_build(ctx: PresubmitContext):
+ gn_gen('--export-compile-commands',
+ _CLANG_GEN_ARGS,
+ path=ctx.output_directory,
+ repo=ctx.repository_root)
+ ninja(path=ctx.output_directory, repo=ctx.repository_root)
@filter_paths(endswith=format_code.C_FORMAT.extensions)
-def gn_gcc_build(unused_ctx: PresubmitContext):
- gn_gen(
- presubmit_dir('gcc'),
- gn_args(pw_target_config='"//targets/host/host.gni"',
- pw_target_toolchain='"//pw_toolchain:host_gcc_os"'))
- call('ninja', '-C', presubmit_dir('gcc'))
+def gn_gcc_build(ctx: PresubmitContext):
+ gn_gen(gn_args(pw_target_config='"//targets/host/host.gni"',
+ pw_target_toolchain='"//pw_toolchain:host_gcc_os"'),
+ path=ctx.output_directory,
+ repo=ctx.repository_root)
+ ninja(path=ctx.output_directory, repo=ctx.repository_root)
-_ARM_GEN_ARGS = (
- presubmit_dir('arm'),
- gn_args(pw_target_config='"//targets/stm32f429i-disc1/target_config.gni"'))
+_ARM_GEN_ARGS = gn_args(
+ pw_target_config='"//targets/stm32f429i-disc1/target_config.gni"')
@filter_paths(endswith=format_code.C_FORMAT.extensions)
-def gn_arm_build(unused_ctx: PresubmitContext):
- gn_gen(*_ARM_GEN_ARGS)
- call('ninja', '-C', presubmit_dir('arm'))
+def gn_arm_build(ctx: PresubmitContext):
+ gn_gen(_ARM_GEN_ARGS, path=ctx.output_directory, repo=ctx.repository_root)
+ ninja(path=ctx.output_directory, repo=ctx.repository_root)
GN = (
@@ -149,11 +148,13 @@
#
@filter_paths(endswith=format_code.C_FORMAT.extensions)
def clang_tidy(ctx: PresubmitContext):
- if not os.path.exists(presubmit_dir('clang', 'compile_commands.json')):
+ # TODO(mohrr) should this check just do a new clang build?
+ out = ctx.output_directory.joinpath('..', 'gn_clang_build')
+ if not out.joinpath('compile_commands.json').exists():
raise PresubmitFailure('clang_tidy MUST be run after generating '
'compile_commands.json in a clang build!')
- call('clang-tidy', f'-p={presubmit_dir("clang")}', *ctx.paths)
+ call('clang-tidy', f'-p={out}', *ctx.paths)
CC = (
@@ -168,7 +169,8 @@
#
@filter_paths(endswith='.py')
def test_python_packages(ctx: PresubmitContext):
- packages = pw_presubmit.find_python_packages(ctx.paths)
+ packages = pw_presubmit.find_python_packages(ctx.paths,
+ repo=ctx.repository_root)
if not packages:
_LOG.info('No Python packages were found.')
@@ -194,6 +196,7 @@
'--jobs=0',
f'--disable={",".join(disable_checkers)}',
*ctx.paths,
+ cwd=ctx.repository_root,
)
@@ -214,10 +217,13 @@
# Bazel presubmit checks
#
@filter_paths(endswith=format_code.C_FORMAT.extensions)
-def bazel_test(unused_paths):
- prefix = '.presubmit/bazel-'
- call('bazel', 'build', '//...', '--symlink_prefix', prefix)
- call('bazel', 'test', '//...', '--symlink_prefix', prefix)
+def bazel_test(ctx: PresubmitContext):
+ call('bazel',
+ 'test',
+ '//...',
+ '--symlink_prefix',
+ ctx.output_directory.joinpath('bazel-'),
+ cwd=ctx.repository_root)
BAZEL = (bazel_test, )
@@ -261,6 +267,7 @@
errors = []
for path in ctx.paths:
+ path = ctx.repository_root.joinpath(path)
with open(path) as file:
# Skip shebang and blank lines
line = file.readline()
@@ -292,14 +299,19 @@
#
-def _get_paths_from_command(*args):
+def _get_paths_from_command(*args, cwd: pathlib.Path, **kwargs):
"""Runs a command and reads Bazel or GN //-style paths from it."""
- process = log_run(*args, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL)
+ process = log_run(*args,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.DEVNULL,
+ cwd=cwd,
+ **kwargs)
files = set()
for line in process.stdout.splitlines():
path = line.strip().lstrip(b'/').replace(b':', b'/').decode()
- if os.path.isfile(path):
+ path = cwd.joinpath(path)
+ if path.is_file():
files.add(path)
return files
@@ -313,21 +325,34 @@
"""Checks that source files are in the GN and Bazel builds."""
# Collect all paths in the Bazel build.
- build = _get_paths_from_command('bazel', 'query',
- 'kind("source file", //...:*)')
+ build_bazel = _get_paths_from_command('bazel',
+ 'query',
+ 'kind("source file", //...:*)',
+ cwd=ctx.repository_root)
# Collect all paths in the ARM and Clang GN builds.
- gn_gen(*_ARM_GEN_ARGS)
- build_gn = _get_paths_from_command('gn', 'desc', presubmit_dir('arm'), '*')
- gn_gen(*_CLANG_GEN_ARGS)
+ arm_dir = ctx.output_directory.joinpath('arm')
+ gn_gen(_ARM_GEN_ARGS, path=arm_dir, repo=ctx.repository_root)
+ build_gn = _get_paths_from_command('gn',
+ 'desc',
+ arm_dir,
+ '*',
+ cwd=ctx.repository_root)
+
+ clang_dir = ctx.output_directory.joinpath('clang')
+ gn_gen(_CLANG_GEN_ARGS, path=clang_dir, repo=ctx.repository_root)
build_gn.update(
- _get_paths_from_command('gn', 'desc', presubmit_dir('clang'), '*'))
+ _get_paths_from_command('gn',
+ 'desc',
+ clang_dir,
+ '*',
+ cwd=ctx.repository_root))
missing_bazel = []
missing_gn = []
- for path in (p for p in ctx.paths if p.endswith(_SOURCES_IN_BUILD)):
- if not path.endswith('.rst') and path not in build:
+ for path in (p for p in ctx.paths if p.suffix in _SOURCES_IN_BUILD):
+ if path.suffix != '.rst' and path not in build_bazel:
missing_bazel.append(path)
if path not in build_gn:
missing_gn.append(path)
@@ -336,7 +361,8 @@
for build, files in [('Bazel', missing_bazel), ('GN', missing_gn)]:
if files:
_LOG.warning('%s are missing from the %s build:\n%s',
- plural(files, 'file'), build, '\n'.join(files))
+ plural(files, 'file'), build,
+ '\n'.join(str(x) for x in files))
_LOG.warning(
'All source files must appear in BUILD and BUILD.gn files')
@@ -370,13 +396,22 @@
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
+ '-o',
+ '--output-directory',
+ type=pathlib.Path,
+ help='Output directory (default: <repo root>/.presubmit)',
+ )
+
+ parser.add_argument(
'--clean',
action='store_true',
- help='Delete the entire .presubmit directory before starting.')
+ help='Delete the entire output directory before starting.',
+ )
parser.add_argument(
'--clean-py',
action='store_true',
- help='Delete the Python virtualenv at .presubmit/venv before starting.'
+ help=('Delete the Python virtualenv in the output directory before '
+ 'starting.'),
)
exclusive = parser.add_mutually_exclusive_group()
@@ -408,13 +443,17 @@
clean: bool,
clean_py: bool,
install: bool,
- directory: str,
+ repository: str,
+ output_directory: str,
step: Sequence[str],
**presubmit_args,
) -> int:
"""Entry point for presubmit."""
- environment = pw_presubmit.git_repo_path(PRESUBMIT_PREFIX, repo=directory)
+ if not output_directory:
+ output_directory = pw_presubmit.git_repo_path('.presubmit',
+ repo=repository)
+ environment = output_directory
_LOG.debug('Using environment at %s', environment)
if clean and environment.exists():
@@ -424,7 +463,7 @@
if install:
install_hook(__file__, 'pre-push', ['--base', 'origin/master..HEAD'],
- directory)
+ repository)
return 0
program = PROGRAMS[program_name]
@@ -432,7 +471,8 @@
program = [x for x in PROGRAMS['full'] if x.__name__ in step]
if pw_presubmit.run_presubmit(program,
- directory=directory,
+ repository=repository,
+ output_directory=output_directory,
**presubmit_args):
return 0
diff --git a/pw_presubmit/py/pw_presubmit/tools.py b/pw_presubmit/py/pw_presubmit/tools.py
index 358b09e..25dc3bb 100644
--- a/pw_presubmit/py/pw_presubmit/tools.py
+++ b/pw_presubmit/py/pw_presubmit/tools.py
@@ -94,9 +94,9 @@
paths: Sequence[str] = (),
repo: str = '.') -> Sequence[str]:
"""Returns absolute paths of files changed since the specified commit."""
- root = git_repo_path()
+ root = git_repo_path(repo=repo)
return [
- os.path.join(root, path) for path in git_stdout(
+ os.path.abspath(os.path.join(root, path)) for path in git_stdout(
'diff', '--name-only', commit, '--', *paths, repo=repo).split()
]
@@ -109,7 +109,7 @@
) -> Sequence[str]:
"""Lists files with git ls-files or git diff --name-only.
- This function may only be called if os.getcwd() is in a Git repository.
+ This function may only be called if repo is or is in a Git repository.
"""
if commit:
@@ -235,13 +235,16 @@
class PresubmitContext:
"""Context passed into presubmit checks."""
repository_root: pathlib.Path
+ output_directory: pathlib.Path
paths: List[str]
class Presubmit:
"""Runs a series of presubmit checks on a list of files."""
- def __init__(self, repository_root: pathlib.Path, paths: List[str]):
+ def __init__(self, repository_root: pathlib.Path,
+ output_directory: pathlib.Path, paths: List[str]):
self._repository_root = repository_root
+ self._output_directory = output_directory
self._paths = paths
def run(self, full_program: Sequence, keep_going: bool = False) -> bool:
@@ -267,9 +270,14 @@
return not failed and not skipped
- def _create_context(self, paths):
- return PresubmitContext(repository_root=self._repository_root,
- paths=paths)
+ def _create_context(self, name, paths):
+ sanitized_name = re.sub(r'[\W_]+', '_', name).lower()
+ output_directory = self._output_directory.joinpath(sanitized_name)
+ os.makedirs(output_directory, exist_ok=True)
+ return PresubmitContext(
+ repository_root=self._repository_root.absolute(),
+ output_directory=output_directory.absolute(),
+ paths=paths)
def _execute_checks(self, program,
keep_going: bool) -> Tuple[int, int, int]:
@@ -277,7 +285,8 @@
passed = failed = 0
for i, (check, paths) in enumerate(program, 1):
- ctx = self._create_context(paths=paths)
+ paths = [self._repository_root.joinpath(p) for p in paths]
+ ctx = self._create_context(check.name, paths=paths)
result = check.run(ctx, i, len(program))
if result is _Result.PASS:
@@ -377,8 +386,8 @@
add_path_arguments(parser)
parser.add_argument(
- '-C',
- dest='directory',
+ '-r',
+ '--repository',
default='.',
help=(
'Change to this directory before resolving paths or running the '
@@ -393,7 +402,8 @@
base: Optional[str] = None,
paths: Sequence[str] = (),
exclude: Sequence = (),
- directory=None,
+ repository=None,
+ output_directory=None,
keep_going: bool = False) -> bool:
"""Lists files in the current Git repo and runs a Presubmit with them.
@@ -405,30 +415,35 @@
base: optional base Git commit to list files against
paths: optional list of paths to run the presubmit checks against
exclude: regular expressions of paths to exclude from checks
- directory: change to this directory before resolving paths or running
+ repository: git repository to check
+ output_directory: where to place output files
keep_going: whether to continue running checks if an error occurs
Returns:
True if all presubmit checks succeeded
"""
- if directory:
- os.chdir(directory)
- if not is_git_repo():
+ if not is_git_repo(repository):
_LOG.critical('Presubmit checks must be run from a Git repo')
return False
- files = list_git_files(base, paths, exclude)
- root = git_repo_path()
+ files = list_git_files(base, paths, exclude, repository)
+ root = git_repo_path(repo=repository)
- if not root.samefile(pathlib.Path.cwd()):
+ if not root.samefile(repository):
_LOG.info('Checking files in the %s subdirectory of the %s repository',
pathlib.Path.cwd().relative_to(root), root)
- os.chdir(root)
files = [os.path.relpath(path, root) for path in files]
- presubmit = Presubmit(repository_root=root, paths=files)
+ if not output_directory:
+ output_directory = root.joinpath('.presubmit')
+
+ presubmit = Presubmit(
+ repository_root=root,
+ output_directory=output_directory,
+ paths=files,
+ )
return presubmit.run(program, keep_going)
@@ -446,11 +461,11 @@
return run_presubmit(program, **vars(arg_parser.parse_args()))
-def find_python_packages(python_paths) -> Dict[str, List[str]]:
+def find_python_packages(python_paths, repo='.') -> Dict[str, List[str]]:
"""Returns Python package directories for the files in python_paths."""
setup_pys = [
- os.path.dirname(file) for file in _git_ls_files(
- 'setup.py', '*/setup.py', repo=git_repo_path())
+ os.path.dirname(file)
+ for file in _git_ls_files('setup.py', '*/setup.py', repo=repo)
]
package_dirs: Dict[str, List[str]] = defaultdict(list)
@@ -561,7 +576,7 @@
"""Logs a command then runs it with subprocess.run."""
_LOG.debug('[COMMAND] %s\n%s',
', '.join(f'{k}={v}' for k, v in sorted(kwargs.items())),
- ' '.join(shlex.quote(arg) for arg in args))
+ ' '.join(shlex.quote(str(arg)) for arg in args))
return subprocess.run(args, **kwargs)
@@ -575,7 +590,7 @@
log('[COMMAND] %s\n%s',
', '.join(f'{k}={v}' for k, v in sorted(kwargs.items())),
- ' '.join(shlex.quote(arg) for arg in args))
+ ' '.join(shlex.quote(str(arg)) for arg in args))
log('[RESULT] %s with return code %d',
'Failed' if process.returncode else 'Passed', process.returncode)