#!/usr/bin/python # Copyright (c) 2012, Franck Cuny # Copyright (c) 2021, Alexei Znamensky # GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) # SPDX-License-Identifier: GPL-3.0-or-later from __future__ import annotations DOCUMENTATION = r""" module: cpanm short_description: Manages Perl library dependencies description: - Manage Perl library dependencies using cpanminus. extends_documentation_fragment: - community.general.attributes attributes: check_mode: support: none diff_mode: support: none options: name: type: str description: - The Perl library to install. Valid values change according to the O(mode), see notes for more details. - Note that for installing from a local path the parameter O(from_path) should be used. aliases: [pkg] from_path: type: path description: - The local directory or C(tar.gz) file to install from. notest: description: - Do not run unit tests. type: bool default: false locallib: description: - Specify the install base to install modules. type: path mirror: description: - Specifies the base URL for the CPAN mirror to use. type: str mirror_only: description: - Use the mirror's index file instead of the CPAN Meta DB. type: bool default: false installdeps: description: - Only install dependencies. type: bool default: false install_recommendations: description: - If V(true), installs dependencies declared as recommends per META spec. - If V(false), it ensures the dependencies declared as recommends are not installed, overriding any decision made earlier in E(PERL_CPANM_OPT). - If parameter is not set, C(cpanm) uses its existing defaults. - When these dependencies fail to install, cpanm continues the installation, since they are just recommendation. type: bool version_added: 10.3.0 install_suggestions: description: - If V(true), installs dependencies declared as suggests per META spec. - If V(false), it ensures the dependencies declared as suggests are not installed, overriding any decision made earlier in E(PERL_CPANM_OPT). - If parameter is not set, C(cpanm) uses its existing defaults. - When these dependencies fail to install, C(cpanm) continues the installation, since they are just suggestions. type: bool version_added: 10.3.0 version: description: - Version specification for the perl module. When O(mode=new), C(cpanm) version operators are accepted. type: str executable: description: - Override the path to the C(cpanm) executable. type: path mode: description: - Controls the module behavior. See notes below for more details. - The default changed from V(compatibility) to V(new) in community.general 9.0.0. V(compatibility) was removed from community.general 13.0.0. - 'O(mode=new): The O(name) parameter may refer to a module name, a distribution file, a HTTP URL or a git repository URL as described in C(cpanminus) documentation. C(cpanm) version specifiers are recognized. This is the default mode from community.general 9.0.0 onwards.' type: str choices: [new] default: new version_added: 3.0.0 name_check: description: - When O(mode=new), this parameter can be used to check if there is a module O(name) installed (at O(version), when specified). type: str version_added: 3.0.0 notes: - Please note that U(http://search.cpan.org/dist/App-cpanminus/bin/cpanm, cpanm) must be installed on the remote host. seealso: - name: C(cpanm) command manual page description: Manual page for the command. link: https://metacpan.org/dist/App-cpanminus/view/bin/cpanm author: - "Franck Cuny (@fcuny)" - "Alexei Znamensky (@russoz)" """ EXAMPLES = r""" - name: Install Dancer perl package community.general.cpanm: name: Dancer - name: Install version 0.99_05 of the Plack perl package community.general.cpanm: name: MIYAGAWA/Plack-0.99_05.tar.gz - name: Install Dancer into the specified locallib community.general.cpanm: name: Dancer locallib: /srv/webapps/my_app/extlib - name: Install perl dependencies from local directory community.general.cpanm: from_path: /srv/webapps/my_app/src/ - name: Install Dancer perl package without running the unit tests in indicated locallib community.general.cpanm: name: Dancer notest: true locallib: /srv/webapps/my_app/extlib - name: Install Dancer perl package from a specific mirror community.general.cpanm: name: Dancer mirror: 'http://cpan.cpantesters.org/' - name: Install Dancer perl package into the system root path become: true community.general.cpanm: name: Dancer - name: Install Dancer if it is not already installed OR the installed version is older than version 1.0 community.general.cpanm: name: Dancer version: '1.0' """ RETURN = r""" cpanm_version: description: Version of CPANMinus. type: str returned: always sample: "1.7047" version_added: 10.0.0 """ import os import re from ansible_collections.community.general.plugins.module_utils.cmd_runner import CmdRunner, cmd_runner_fmt from ansible_collections.community.general.plugins.module_utils.module_helper import ModuleHelper class CPANMinus(ModuleHelper): output_params = ["name", "version"] module = dict( argument_spec=dict( name=dict(type="str", aliases=["pkg"]), version=dict(type="str"), from_path=dict(type="path"), notest=dict(type="bool", default=False), locallib=dict(type="path"), mirror=dict(type="str"), mirror_only=dict(type="bool", default=False), installdeps=dict(type="bool", default=False), install_recommendations=dict(type="bool"), install_suggestions=dict(type="bool"), executable=dict(type="path"), mode=dict(type="str", default="new", choices=["new"]), name_check=dict(type="str"), ), required_one_of=[("name", "from_path")], ) command = "cpanm" command_args_formats = dict( notest=cmd_runner_fmt.as_bool("--notest"), locallib=cmd_runner_fmt.as_opt_val("--local-lib"), mirror=cmd_runner_fmt.as_opt_val("--mirror"), mirror_only=cmd_runner_fmt.as_bool("--mirror-only"), installdeps=cmd_runner_fmt.as_bool("--installdeps"), install_recommendations=cmd_runner_fmt.as_bool("--with-recommends", "--without-recommends", ignore_none=True), install_suggestions=cmd_runner_fmt.as_bool("--with-suggests", "--without-suggests", ignore_none=True), pkg_spec=cmd_runner_fmt.as_list(), cpanm_version=cmd_runner_fmt.as_fixed("--version"), ) def __init_module__(self): v = self.vars if v.name and v.from_path: self.do_raise("Parameters 'name' and 'from_path' are mutually exclusive when 'mode=new'") self.command = v.executable if v.executable else self.command self.runner = CmdRunner(self.module, self.command, self.command_args_formats, check_rc=True) self.vars.binary = self.runner.binary with self.runner("cpanm_version") as ctx: rc, out, err = ctx.run() line = out.split("\n")[0] match = re.search(r"version\s+([\d\.]+)\s+", line) if not match: self.do_raise(f"Failed to determine version number. First line of output: {line}") self.vars.cpanm_version = match.group(1) def _is_package_installed(self, name, locallib, version): def process(rc, out, err): return rc == 0 if name is None or name.endswith(".tar.gz"): return False version = "" if version is None else f" {version}" env = {"PERL5LIB": f"{locallib}/lib/perl5"} if locallib else {} runner = CmdRunner( self.module, ["perl", "-le"], {"mod": cmd_runner_fmt.as_list()}, check_rc=False, environ_update=env ) with runner("mod", output_process=process) as ctx: return ctx.run(mod=f"use {name}{version};") def sanitize_pkg_spec_version(self, pkg_spec, version): if version is None: return pkg_spec if pkg_spec.endswith(".tar.gz"): self.do_raise(msg="parameter 'version' must not be used when installing from a file") if os.path.isdir(pkg_spec): self.do_raise(msg="parameter 'version' must not be used when installing from a directory") if pkg_spec.endswith(".git"): if version.startswith("~"): self.do_raise(msg="operator '~' not allowed in version parameter when installing from git repository") version = version if version.startswith("@") else f"@{version}" elif version[0] not in ("@", "~"): version = f"~{version}" return pkg_spec + version def __run__(self): def process(rc, out, err): return "is up to date" not in err and "is up to date" not in out v = self.vars pkg_param = "from_path" if v.from_path else "name" installed = self._is_package_installed(v.name_check, v.locallib, v.version) if v.name_check else False if installed: return pkg_spec = self.sanitize_pkg_spec_version(v[pkg_param], v.version) with self.runner( [ "notest", "locallib", "mirror", "mirror_only", "installdeps", "install_recommendations", "install_suggestions", "pkg_spec", ], output_process=process, ) as ctx: self.changed = ctx.run(pkg_spec=pkg_spec) def main(): CPANMinus.execute() if __name__ == "__main__": main()