diff --git a/changelogs/fragments/8480-directory-feature-cargo.yml b/changelogs/fragments/8480-directory-feature-cargo.yml new file mode 100644 index 0000000000..8892e7c5dd --- /dev/null +++ b/changelogs/fragments/8480-directory-feature-cargo.yml @@ -0,0 +1,2 @@ +minor_changes: + - "cargo - add option ``directory``, which allows source directory to be specified (https://github.com/ansible-collections/community.general/pull/8480)." diff --git a/plugins/modules/cargo.py b/plugins/modules/cargo.py index ba9c05ed7b..2fc729da20 100644 --- a/plugins/modules/cargo.py +++ b/plugins/modules/cargo.py @@ -1,6 +1,7 @@ #!/usr/bin/python # -*- coding: utf-8 -*- # Copyright (c) 2021 Radek Sprta +# Copyright (c) 2024 Colin Nolan # 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 @@ -65,6 +66,13 @@ options: type: str default: present choices: [ "present", "absent", "latest" ] + directory: + description: + - Path to the source directory to install the Rust package from. + - This is only used when installing packages. + type: path + required: false + version_added: 9.1.0 requirements: - cargo installed """ @@ -98,8 +106,14 @@ EXAMPLES = r""" community.general.cargo: name: ludusavi state: latest + +- name: Install "ludusavi" Rust package from source directory + community.general.cargo: + name: ludusavi + directory: /path/to/ludusavi/source """ +import json import os import re @@ -115,6 +129,7 @@ class Cargo(object): self.state = kwargs["state"] self.version = kwargs["version"] self.locked = kwargs["locked"] + self.directory = kwargs["directory"] @property def path(self): @@ -143,7 +158,7 @@ class Cargo(object): data, dummy = self._exec(cmd, True, False, False) - package_regex = re.compile(r"^([\w\-]+) v(.+):$") + package_regex = re.compile(r"^([\w\-]+) v(\S+).*:$") installed = {} for line in data.splitlines(): package_info = package_regex.match(line) @@ -163,19 +178,53 @@ class Cargo(object): if self.version: cmd.append("--version") cmd.append(self.version) + if self.directory: + cmd.append("--path") + cmd.append(self.directory) return self._exec(cmd) def is_outdated(self, name): installed_version = self.get_installed().get(name) + latest_version = ( + self.get_latest_published_version(name) + if not self.directory + else self.get_source_directory_version(name) + ) + return installed_version != latest_version + def get_latest_published_version(self, name): cmd = ["search", name, "--limit", "1"] data, dummy = self._exec(cmd, True, False, False) match = re.search(r'"(.+)"', data) - if match: - latest_version = match.group(1) + if not match: + self.module.fail_json( + msg="No published version for package %s found" % name + ) + return match.group(1) - return installed_version != latest_version + def get_source_directory_version(self, name): + cmd = [ + "metadata", + "--format-version", + "1", + "--no-deps", + "--manifest-path", + os.path.join(self.directory, "Cargo.toml"), + ] + data, dummy = self._exec(cmd, True, False, False) + manifest = json.loads(data) + + package = next( + (package for package in manifest["packages"] if package["name"] == name), + None, + ) + if not package: + self.module.fail_json( + msg="Package %s not defined in source, found: %s" + % (name, [x["name"] for x in manifest["packages"]]) + ) + return package["version"] def uninstall(self, packages=None): cmd = ["uninstall"] @@ -191,16 +240,21 @@ def main(): state=dict(default="present", choices=["present", "absent", "latest"]), version=dict(default=None, type="str"), locked=dict(default=False, type="bool"), + directory=dict(default=None, type="path"), ) module = AnsibleModule(argument_spec=arg_spec, supports_check_mode=True) name = module.params["name"] state = module.params["state"] version = module.params["version"] + directory = module.params["directory"] if not name: module.fail_json(msg="Package name must be specified") + if directory is not None and not os.path.isdir(directory): + module.fail_json(msg="Source directory does not exist") + # Set LANG env since we parse stdout module.run_command_environ_update = dict( LANG="C", LC_ALL="C", LC_MESSAGES="C", LC_CTYPE="C" diff --git a/tests/integration/targets/cargo/tasks/main.yml b/tests/integration/targets/cargo/tasks/main.yml index 29f27c3fda..89f13960a6 100644 --- a/tests/integration/targets/cargo/tasks/main.yml +++ b/tests/integration/targets/cargo/tasks/main.yml @@ -16,6 +16,7 @@ - block: - import_tasks: test_general.yml - import_tasks: test_version.yml + - import_tasks: test_directory.yml environment: "{{ cargo_environment }}" when: has_cargo | default(false) - import_tasks: test_rustup_cargo.yml diff --git a/tests/integration/targets/cargo/tasks/test_directory.yml b/tests/integration/targets/cargo/tasks/test_directory.yml new file mode 100644 index 0000000000..f4275ede68 --- /dev/null +++ b/tests/integration/targets/cargo/tasks/test_directory.yml @@ -0,0 +1,122 @@ +--- +# Copyright (c) 2024 Colin Nolan +# 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 + +- name: Create temp directory + tempfile: + state: directory + register: temp_directory + +- name: Test block + vars: + manifest_path: "{{ temp_directory.path }}/Cargo.toml" + package_name: hello-world-directory-test + block: + - name: Initialize package + ansible.builtin.command: + cmd: "cargo init --name {{ package_name }}" + args: + chdir: "{{ temp_directory.path }}" + + - name: Set package version (1.0.0) + ansible.builtin.lineinfile: + path: "{{ manifest_path }}" + regexp: '^version = ".*"$' + line: 'version = "1.0.0"' + + - name: Ensure package is uninstalled + community.general.cargo: + name: "{{ package_name }}" + state: absent + directory: "{{ temp_directory.path }}" + register: uninstall_absent + + - name: Install package + community.general.cargo: + name: "{{ package_name }}" + directory: "{{ temp_directory.path }}" + register: install_absent + + - name: Change package version (1.0.1) + ansible.builtin.lineinfile: + path: "{{ manifest_path }}" + regexp: '^version = ".*"$' + line: 'version = "1.0.1"' + + - name: Install package again (present) + community.general.cargo: + name: "{{ package_name }}" + state: present + directory: "{{ temp_directory.path }}" + register: install_present_state + + - name: Install package again (latest) + community.general.cargo: + name: "{{ package_name }}" + state: latest + directory: "{{ temp_directory.path }}" + register: install_latest_state + + - name: Change package version (2.0.0) + ansible.builtin.lineinfile: + path: "{{ manifest_path }}" + regexp: '^version = ".*"$' + line: 'version = "2.0.0"' + + - name: Install package with given version (matched) + community.general.cargo: + name: "{{ package_name }}" + version: "2.0.0" + directory: "{{ temp_directory.path }}" + register: install_given_version_matched + + - name: Install package with given version (unmatched) + community.general.cargo: + name: "{{ package_name }}" + version: "2.0.1" + directory: "{{ temp_directory.path }}" + register: install_given_version_unmatched + ignore_errors: true + + - name: Uninstall package + community.general.cargo: + name: "{{ package_name }}" + state: absent + directory: "{{ temp_directory.path }}" + register: uninstall_present + + - name: Install non-existant package + community.general.cargo: + name: "{{ package_name }}-non-existant" + state: present + directory: "{{ temp_directory.path }}" + register: install_non_existant + ignore_errors: true + + - name: Install non-existant source directory + community.general.cargo: + name: "{{ package_name }}" + state: present + directory: "{{ temp_directory.path }}/non-existant" + register: install_non_existant_source + ignore_errors: true + + always: + - name: Remove temp directory + file: + path: "{{ temp_directory.path }}" + state: absent + +- name: Check assertions + assert: + that: + - uninstall_absent is not changed + - install_absent is changed + - install_present_state is not changed + - install_latest_state is changed + - install_given_version_matched is changed + - install_given_version_unmatched is failed + - uninstall_present is changed + - install_non_existant is failed + - install_non_existant_source is failed