diff --git a/docs/usage.rst b/docs/usage.rst index 36134d6ec7..c233a2efb6 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -23,6 +23,19 @@ associated tags. :cwd: .. :returncode: 0 +Temporary files +--------------- + +As part of the execution, the linter will likely need to create a cache of +installed or mocked roles, collections and modules. This is done inside +``{project_dir}/.cache`` folder. The project directory is either given as a +command line argument, determined by location of the configuration +file, git project top-level directiory or user home directory as fallback. +In order to speed-up reruns, the linter does not clean this folder by itself. + +If you are using git, you will likely want to add this folder to your +``.gitignore`` file. + Progressive mode ---------------- diff --git a/src/ansiblelint/_prerun.py b/src/ansiblelint/_prerun.py index da441f5c07..5444a93429 100644 --- a/src/ansiblelint/_prerun.py +++ b/src/ansiblelint/_prerun.py @@ -99,7 +99,7 @@ def prepare_environment() -> None: "role", "install", "--roles-path", - ".cache/roles", + f"{options.project_dir}/.cache/roles", "-vr", "requirements.yml", ] @@ -124,7 +124,7 @@ def prepare_environment() -> None: "collection", "install", "-p", - ".cache/collections", + f"{options.project_dir}/.cache/collections", "-vr", "requirements.yml", ] @@ -175,7 +175,7 @@ def _install_galaxy_role() -> None: file=sys.stderr, ) sys.exit(INVALID_PREREQUISITES_RC) - p = pathlib.Path(".cache/roles") + p = pathlib.Path(f"{options.project_dir}/.cache/roles") p.mkdir(parents=True, exist_ok=True) link_path = p / f"{role_author}.{role_name}" # despite documentation stating that is_file() reports true for symlinks, @@ -195,10 +195,10 @@ def _prepare_ansible_paths() -> None: for path_list, path in ( (library_paths, "plugins/modules"), - (library_paths, ".cache/modules"), - (collection_list, ".cache/collections"), + (library_paths, f"{options.project_dir}/.cache/modules"), + (collection_list, f"{options.project_dir}/.cache/collections"), (roles_path, "roles"), - (roles_path, ".cache/roles"), + (roles_path, f"{options.project_dir}/.cache/roles"), ): if path not in path_list and os.path.exists(path): path_list.append(path) @@ -212,7 +212,7 @@ def _make_module_stub(module_name: str) -> None: # a.b.c is treated a collection if re.match(r"\w+\.\w+\.\w+", module_name): namespace, collection, module_file = module_name.split(".") - path = f".cache/collections/ansible_collections/{ namespace }/{ collection }/plugins/modules" + path = f"{ options.project_dir }/.cache/collections/ansible_collections/{ namespace }/{ collection }/plugins/modules" os.makedirs(path, exist_ok=True) _write_module_stub( filename=f"{path}/{module_file}.py", @@ -227,9 +227,10 @@ def _make_module_stub(module_name: str) -> None: ) sys.exit(INVALID_CONFIG_RC) else: - os.makedirs(".cache/modules", exist_ok=True) + os.makedirs(f"{options.project_dir}/.cache/modules", exist_ok=True) _write_module_stub( - filename=f".cache/modules/{module_name}.py", name=module_name + filename=f"{options.project_dir}/.cache/modules/{module_name}.py", + name=module_name, ) diff --git a/src/ansiblelint/cli.py b/src/ansiblelint/cli.py index 4434725dec..47bc97f0a0 100644 --- a/src/ansiblelint/cli.py +++ b/src/ansiblelint/cli.py @@ -15,7 +15,7 @@ DEFAULT_RULESDIR, INVALID_CONFIG_RC, ) -from ansiblelint.file_utils import expand_path_vars +from ansiblelint.file_utils import expand_path_vars, guess_project_dir, normpath _logger = logging.getLogger(__name__) _PATH_VARS = [ @@ -82,6 +82,8 @@ def load_config(config_file: str) -> Dict[Any, Any]: except yaml.YAMLError as e: _logger.error(e) sys.exit(INVALID_CONFIG_RC) + + config['config_file'] = config_path # TODO(ssbarnea): implement schema validation for config file if isinstance(config, list): _logger.error( @@ -92,6 +94,7 @@ def load_config(config_file: str) -> Dict[Any, Any]: config_dir = os.path.dirname(config_path) expand_to_normalized_paths(config, config_dir) + return config @@ -164,6 +167,13 @@ def get_cli_parser() -> argparse.ArgumentParser: " of violations compared with previous git commit. This " "feature works only in git repositories.", ) + parser.add_argument( + '--project-dir', + dest='project_dir', + default=None, + help="Location of project/repository, autodetected based on location " + " of configuration file.", + ) parser.add_argument( '-r', action=AbspathArgAction, @@ -289,7 +299,7 @@ def merge_config(file_config, cli_config: Namespace) -> Namespace: 'mock_roles': [], } - scalar_map = {"loop_var_prefix": None} + scalar_map = {"loop_var_prefix": None, "project_dir": None} if not file_config: # use defaults if we don't have a config file and the commandline @@ -337,6 +347,14 @@ def get_config(arguments: List[str]) -> Namespace: options.rulesdirs = get_rules_dirs(options.rulesdir, options.use_default_rules) + if not options.project_dir: + project_dir = os.path.dirname( + os.path.abspath( + options.config_file or f"{guess_project_dir()}/.ansiblelint" + ) + ) + options.project_dir = normpath(project_dir) + return config diff --git a/src/ansiblelint/config.py b/src/ansiblelint/config.py index 26ab30172a..acfdbb6511 100644 --- a/src/ansiblelint/config.py +++ b/src/ansiblelint/config.py @@ -54,6 +54,7 @@ mock_roles=[], loop_var_prefix=None, offline=False, + project_dir=None, extra_vars=None, skip_action_validation=True, ) diff --git a/src/ansiblelint/file_utils.py b/src/ansiblelint/file_utils.py index 8a88b7475d..1f7244ae19 100644 --- a/src/ansiblelint/file_utils.py +++ b/src/ansiblelint/file_utils.py @@ -216,6 +216,21 @@ def get_yaml_files(options: Namespace) -> Dict[str, Any]: return OrderedDict.fromkeys(sorted(out)) +def guess_project_dir() -> str: + """Return detected project dir or user home directory.""" + result = subprocess.run( + ["git", "rev-parse", "--show-toplevel"], + stderr=subprocess.PIPE, + stdout=subprocess.PIPE, + universal_newlines=True, + check=False, + ) + + if result.returncode != 0: + return str(Path.home()) + return result.stdout[0] + + def expand_dirs_in_lintables(lintables: Set[Lintable]) -> None: """Return all recognized lintables within given directory.""" all_files = get_yaml_files(options) diff --git a/test/TestCliRolePaths.py b/test/TestCliRolePaths.py index d3a8b718b2..82a050c7e3 100644 --- a/test/TestCliRolePaths.py +++ b/test/TestCliRolePaths.py @@ -108,7 +108,7 @@ def test_run_role_name_with_prefix(self): result = run_ansible_lint(role_path, cwd=cwd) assert len(result.stdout) == 0 assert ( - "Added ANSIBLE_ROLES_PATH=~/.ansible/roles:/usr/share/ansible/roles:/etc/ansible/roles:roles:.cache/roles" + "Added ANSIBLE_ROLES_PATH=~/.ansible/roles:/usr/share/ansible/roles:/etc/ansible/roles:roles" in result.stderr ) assert result.returncode == 0 @@ -120,7 +120,7 @@ def test_run_role_name_from_meta(self): result = run_ansible_lint(role_path, cwd=cwd) assert len(result.stdout) == 0 assert ( - "Added ANSIBLE_ROLES_PATH=~/.ansible/roles:/usr/share/ansible/roles:/etc/ansible/roles:roles:.cache/roles" + "Added ANSIBLE_ROLES_PATH=~/.ansible/roles:/usr/share/ansible/roles:/etc/ansible/roles:roles" in result.stderr ) assert result.returncode == 0 diff --git a/test/TestCommandLineInvocationSameAsConfig.py b/test/TestCommandLineInvocationSameAsConfig.py index 1004de89fd..a3273ed2b5 100644 --- a/test/TestCommandLineInvocationSameAsConfig.py +++ b/test/TestCommandLineInvocationSameAsConfig.py @@ -44,6 +44,11 @@ def _fake_pathlib_resolve(self): file_config = cli.load_config(config) for key, val in file_config.items(): + + # config_file does not make sense in file_config + if key == 'config_file': + continue + if key in {'exclude_paths', 'rulesdir'}: val = [Path(p) for p in val] assert val == getattr(options, key)