-
Notifications
You must be signed in to change notification settings - Fork 1.5k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* adds simple implementation of adding and removing android sdk packages * adds package update * adds simple installed packages parsing * moves parsing logic to a separate class * adds absent state for sdkmanager packages and setup for tests * adds output for installing and removing packages * removes version from Package object since it is not possible to specify version for a package while using sdkmanager * adds 'latest' state * adds tests * fixes crash when sdkmanager is invoked from python with LC_ALL=C * fixes latest state * adds sdk_root parameter * adds channel parameter * simplifies regexps, removes unused named groups * minor refactoring of sdkmanager parsing * adds java dependency variable for different distributions * adds RETURN documentation * adds check for nonexisting package * adds check for non-accepted licenses * removes excessive methods from sdkmanager * removes unused 'update' module parameter, packages may be updated using 'latest' state * minor refactoring * adds EXAMPLES doc section * adds DOCUMENTATION section and license headers * fixes formatting issues * removes diff_params * adds maintainer * fixes sanity check issues in sdkmanager * adds java dependency for macos and moves some tests to a separate FreeBSD configuration * fixes dependencies setup for OSX * fixes dependencies setup for OSX (2) * fixes dependencies setup for OSX (3) * Apply minor suggestions from code review Co-authored-by: Alexei Znamensky <[email protected]> * applies code review suggestions * changes force_lang from C.UTF-8 to auto in sdkmanager (as per discussion #9236 (comment)) * Revert "changes force_lang from C.UTF-8 to auto in sdkmanager (as per discussion #9236 (comment))" This reverts commit 619f28d. * fixes some more comments from review * minor sanity issue fix * uses the 'changed' test instead of checking the 'changed' attribute * adds 'accept_licenses' parameter. Installation is now performed independently for each package specified. * removes "Accept licenses" task from examples * fixes docs sanity issues * applies minor suggestions from code review * fixes regexps. The previous version didn't match versions like "32.1.0 rc1". Also, this allows to simplify the parsing logic as there is no need to skip table headers anymore. * renamed sdkmanager.py to android_sdkmanager.py * applies minor suggestions from code review Co-authored-by: Felix Fontein <[email protected]> * updates BOTMETA * reordered BOTMETA --------- Co-authored-by: Alexei Znamensky <[email protected]> Co-authored-by: Felix Fontein <[email protected]>
- Loading branch information
1 parent
2682ec4
commit 2b2872f
Showing
17 changed files
with
711 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,148 @@ | ||
# -*- coding: utf-8 -*- | ||
|
||
# Copyright (c) 2024, Stanislav Shamilov <[email protected]> | ||
# 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 absolute_import, division, print_function | ||
|
||
__metaclass__ = type | ||
|
||
import re | ||
|
||
from ansible_collections.community.general.plugins.module_utils.cmd_runner import CmdRunner, cmd_runner_fmt | ||
|
||
__state_map = { | ||
"present": "--install", | ||
"absent": "--uninstall" | ||
} | ||
|
||
# sdkmanager --help 2>&1 | grep -A 2 -- --channel | ||
__channel_map = { | ||
"stable": 0, | ||
"beta": 1, | ||
"dev": 2, | ||
"canary": 3 | ||
} | ||
|
||
|
||
def __map_channel(channel_name): | ||
if channel_name not in __channel_map: | ||
raise ValueError("Unknown channel name '%s'" % channel_name) | ||
return __channel_map[channel_name] | ||
|
||
|
||
def sdkmanager_runner(module, **kwargs): | ||
return CmdRunner( | ||
module, | ||
command='sdkmanager', | ||
arg_formats=dict( | ||
state=cmd_runner_fmt.as_map(__state_map), | ||
name=cmd_runner_fmt.as_list(), | ||
installed=cmd_runner_fmt.as_fixed("--list_installed"), | ||
list=cmd_runner_fmt.as_fixed('--list'), | ||
newer=cmd_runner_fmt.as_fixed("--newer"), | ||
sdk_root=cmd_runner_fmt.as_opt_eq_val("--sdk_root"), | ||
channel=cmd_runner_fmt.as_func(lambda x: ["{0}={1}".format("--channel", __map_channel(x))]) | ||
), | ||
force_lang="C.UTF-8", # Without this, sdkmanager binary crashes | ||
**kwargs | ||
) | ||
|
||
|
||
class Package: | ||
def __init__(self, name): | ||
self.name = name | ||
|
||
def __hash__(self): | ||
return hash(self.name) | ||
|
||
def __ne__(self, other): | ||
if not isinstance(other, Package): | ||
return True | ||
return self.name != other.name | ||
|
||
def __eq__(self, other): | ||
if not isinstance(other, Package): | ||
return False | ||
|
||
return self.name == other.name | ||
|
||
|
||
class SdkManagerException(Exception): | ||
pass | ||
|
||
|
||
class AndroidSdkManager(object): | ||
_RE_INSTALLED_PACKAGES_HEADER = re.compile(r'^Installed packages:$') | ||
_RE_UPDATABLE_PACKAGES_HEADER = re.compile(r'^Available Updates:$') | ||
|
||
# Example: ' platform-tools | 27.0.0 | Android SDK Platform-Tools 27 | platform-tools ' | ||
_RE_INSTALLED_PACKAGE = re.compile(r'^\s*(?P<name>\S+)\s*\|\s*[0-9][^|]*\b\s*\|\s*.+\s*\|\s*(\S+)\s*$') | ||
|
||
# Example: ' platform-tools | 27.0.0 | 35.0.2' | ||
_RE_UPDATABLE_PACKAGE = re.compile(r'^\s*(?P<name>\S+)\s*\|\s*[0-9][^|]*\b\s*\|\s*[0-9].*\b\s*$') | ||
|
||
_RE_UNKNOWN_PACKAGE = re.compile(r'^Warning: Failed to find package \'(?P<package>\S+)\'\s*$') | ||
_RE_ACCEPT_LICENSE = re.compile(r'^The following packages can not be installed since their licenses or those of ' | ||
r'the packages they depend on were not accepted') | ||
|
||
def __init__(self, module): | ||
self.runner = sdkmanager_runner(module) | ||
|
||
def get_installed_packages(self): | ||
with self.runner('installed sdk_root channel') as ctx: | ||
rc, stdout, stderr = ctx.run() | ||
return self._parse_packages(stdout, self._RE_INSTALLED_PACKAGES_HEADER, self._RE_INSTALLED_PACKAGE) | ||
|
||
def get_updatable_packages(self): | ||
with self.runner('list newer sdk_root channel') as ctx: | ||
rc, stdout, stderr = ctx.run() | ||
return self._parse_packages(stdout, self._RE_UPDATABLE_PACKAGES_HEADER, self._RE_UPDATABLE_PACKAGE) | ||
|
||
def apply_packages_changes(self, packages, accept_licenses=False): | ||
""" Install or delete packages, depending on the `module.vars.state` parameter """ | ||
if len(packages) == 0: | ||
return 0, '', '' | ||
|
||
if accept_licenses: | ||
license_prompt_answer = 'y' | ||
else: | ||
license_prompt_answer = 'N' | ||
for package in packages: | ||
with self.runner('state name sdk_root channel', data=license_prompt_answer) as ctx: | ||
rc, stdout, stderr = ctx.run(name=package.name) | ||
|
||
for line in stdout.splitlines(): | ||
if self._RE_ACCEPT_LICENSE.match(line): | ||
raise SdkManagerException("Licenses for some packages were not accepted") | ||
|
||
if rc != 0: | ||
self._try_parse_stderr(stderr) | ||
return rc, stdout, stderr | ||
return 0, '', '' | ||
|
||
def _try_parse_stderr(self, stderr): | ||
data = stderr.splitlines() | ||
for line in data: | ||
unknown_package_regex = self._RE_UNKNOWN_PACKAGE.match(line) | ||
if unknown_package_regex: | ||
package = unknown_package_regex.group('package') | ||
raise SdkManagerException("Unknown package %s" % package) | ||
|
||
@staticmethod | ||
def _parse_packages(stdout, header_regexp, row_regexp): | ||
data = stdout.splitlines() | ||
|
||
section_found = False | ||
packages = set() | ||
|
||
for line in data: | ||
if not section_found: | ||
section_found = header_regexp.match(line) | ||
continue | ||
else: | ||
p = row_regexp.match(line) | ||
if p: | ||
packages.add(Package(p.group('name'))) | ||
return packages |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,213 @@ | ||
#!/usr/bin/python | ||
# -*- coding: utf-8 -*- | ||
|
||
# Copyright (c) 2024, Stanislav Shamilov <[email protected]> | ||
# 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 absolute_import, division, print_function | ||
|
||
__metaclass__ = type | ||
|
||
DOCUMENTATION = r''' | ||
--- | ||
module: android_sdk | ||
short_description: Manages Android SDK packages | ||
description: | ||
- Manages Android SDK packages. | ||
- Allows installation from different channels (stable, beta, dev, canary). | ||
- Allows installation of packages to a non-default SDK root directory. | ||
author: Stanislav Shamilov (@shamilovstas) | ||
extends_documentation_fragment: | ||
- community.general.attributes | ||
attributes: | ||
check_mode: | ||
support: full | ||
diff_mode: | ||
support: none | ||
version_added: 10.2.0 | ||
options: | ||
accept_licenses: | ||
description: | ||
- If this is set to B(true), the module will try to accept license prompts generated by C(sdkmanager) during | ||
package installation. Otherwise, every license prompt will be rejected. | ||
type: bool | ||
default: false | ||
name: | ||
description: | ||
- A name of an Android SDK package (for instance, V(build-tools;34.0.0)). | ||
aliases: ['package', 'pkg'] | ||
type: list | ||
elements: str | ||
state: | ||
description: | ||
- Indicates the desired package(s) state. | ||
- V(present) ensures that package(s) is/are present. | ||
- V(absent) ensures that package(s) is/are absent. | ||
- V(latest) ensures that package(s) is/are installed and updated to the latest version(s). | ||
choices: ['present', 'absent', 'latest'] | ||
default: present | ||
type: str | ||
sdk_root: | ||
description: | ||
- Provides path for an alternative directory to install Android SDK packages to. By default, all packages | ||
are installed to the directory where C(sdkmanager) is installed. | ||
type: path | ||
channel: | ||
description: | ||
- Indicates what channel must C(sdkmanager) use for installation of packages. | ||
choices: ['stable', 'beta', 'dev', 'canary'] | ||
default: stable | ||
type: str | ||
requirements: | ||
- C(java) >= 17 | ||
- C(sdkmanager) Command line tool for installing Android SDK packages. | ||
notes: | ||
- For some of the packages installed by C(sdkmanager) is it necessary to accept licenses. Usually it is done through | ||
command line prompt in a form of a Y/N question when a licensed package is requested to be installed. If there are | ||
several packages requested for installation and at least two of them belong to different licenses, the C(sdkmanager) | ||
tool will prompt for these licenses in a loop. | ||
In order to install packages, the module must be able to answer these license prompts. Currently, it is only | ||
possible to answer one license prompt at a time, meaning that instead of installing multiple packages as a single | ||
invocation of the C(sdkmanager --install) command, it will be done by executing the command independently for each | ||
package. This makes sure that at most only one license prompt will need to be answered. | ||
At the time of writing this module, a C(sdkmanager)'s package may belong to at most one license type that needs to | ||
be accepted. However, if this is changes in the future, the module may hang as there might be more prompts generated | ||
by the C(sdkmanager) tool which the module will not be able to answer. If this is the case, file an issue and in the | ||
meantime, consider accepting all the licenses in advance, as it is described in the C(sdkmanager) | ||
L(documentation,https://developer.android.com/tools/sdkmanager#accept-licenses), for instance, using the | ||
M(ansible.builtin.command) module. | ||
seealso: | ||
- name: sdkmanager tool documentation | ||
description: Detailed information of how to install and use sdkmanager command line tool. | ||
link: https://developer.android.com/tools/sdkmanager | ||
''' | ||
|
||
EXAMPLES = r''' | ||
- name: Install build-tools;34.0.0 | ||
community.general.android_sdk: | ||
name: build-tools;34.0.0 | ||
accept_licenses: true | ||
state: present | ||
- name: Install build-tools;34.0.0 and platform-tools | ||
community.general.android_sdk: | ||
name: | ||
- build-tools;34.0.0 | ||
- platform-tools | ||
accept_licenses: true | ||
state: present | ||
- name: Delete build-tools;34.0.0 | ||
community.general.android_sdk: | ||
name: build-tools;34.0.0 | ||
state: absent | ||
- name: Install platform-tools or update if installed | ||
community.general.android_sdk: | ||
name: platform-tools | ||
accept_licenses: true | ||
state: latest | ||
- name: Install build-tools;34.0.0 to a different SDK root | ||
community.general.android_sdk: | ||
name: build-tools;34.0.0 | ||
accept_licenses: true | ||
state: present | ||
sdk_root: "/path/to/new/root" | ||
- name: Install a package from another channel | ||
community.general.android_sdk: | ||
name: some-package-present-in-canary-channel | ||
accept_licenses: true | ||
state: present | ||
channel: canary | ||
''' | ||
|
||
RETURN = r''' | ||
installed: | ||
description: a list of packages that have been installed | ||
returned: when packages have changed | ||
type: list | ||
sample: ['build-tools;34.0.0', 'platform-tools'] | ||
removed: | ||
description: a list of packages that have been removed | ||
returned: when packages have changed | ||
type: list | ||
sample: ['build-tools;34.0.0', 'platform-tools'] | ||
''' | ||
|
||
from ansible_collections.community.general.plugins.module_utils.mh.module_helper import StateModuleHelper | ||
from ansible_collections.community.general.plugins.module_utils.android_sdkmanager import Package, AndroidSdkManager | ||
|
||
|
||
class AndroidSdk(StateModuleHelper): | ||
module = dict( | ||
argument_spec=dict( | ||
state=dict(type='str', default='present', choices=['present', 'absent', 'latest']), | ||
package=dict(type='list', elements='str', aliases=['pkg', 'name']), | ||
sdk_root=dict(type='path'), | ||
channel=dict(type='str', default='stable', choices=['stable', 'beta', 'dev', 'canary']), | ||
accept_licenses=dict(type='bool', default=False) | ||
), | ||
supports_check_mode=True | ||
) | ||
use_old_vardict = False | ||
|
||
def __init_module__(self): | ||
self.sdkmanager = AndroidSdkManager(self.module) | ||
self.vars.set('installed', [], change=True) | ||
self.vars.set('removed', [], change=True) | ||
|
||
def _parse_packages(self): | ||
arg_pkgs = set(self.vars.package) | ||
if len(arg_pkgs) < len(self.vars.package): | ||
self.do_raise("Packages may not repeat") | ||
return set([Package(p) for p in arg_pkgs]) | ||
|
||
def state_present(self): | ||
packages = self._parse_packages() | ||
installed = self.sdkmanager.get_installed_packages() | ||
pending_installation = packages.difference(installed) | ||
|
||
self.vars.installed = AndroidSdk._map_packages_to_names(pending_installation) | ||
if not self.check_mode: | ||
rc, stdout, stderr = self.sdkmanager.apply_packages_changes(pending_installation, self.vars.accept_licenses) | ||
if rc != 0: | ||
self.do_raise("Could not install packages: %s" % stderr) | ||
|
||
def state_absent(self): | ||
packages = self._parse_packages() | ||
installed = self.sdkmanager.get_installed_packages() | ||
to_be_deleted = packages.intersection(installed) | ||
self.vars.removed = AndroidSdk._map_packages_to_names(to_be_deleted) | ||
if not self.check_mode: | ||
rc, stdout, stderr = self.sdkmanager.apply_packages_changes(to_be_deleted) | ||
if rc != 0: | ||
self.do_raise("Could not uninstall packages: %s" % stderr) | ||
|
||
def state_latest(self): | ||
packages = self._parse_packages() | ||
installed = self.sdkmanager.get_installed_packages() | ||
updatable = self.sdkmanager.get_updatable_packages() | ||
not_installed = packages.difference(installed) | ||
to_be_installed = not_installed.union(updatable) | ||
self.vars.installed = AndroidSdk._map_packages_to_names(to_be_installed) | ||
|
||
if not self.check_mode: | ||
rc, stdout, stderr = self.sdkmanager.apply_packages_changes(to_be_installed, self.vars.accept_licenses) | ||
if rc != 0: | ||
self.do_raise("Could not install packages: %s" % stderr) | ||
|
||
@staticmethod | ||
def _map_packages_to_names(packages): | ||
return [x.name for x in packages] | ||
|
||
|
||
def main(): | ||
AndroidSdk.execute() | ||
|
||
|
||
if __name__ == '__main__': | ||
main() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
# Copyright (c) Ansible Project | ||
# 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 | ||
|
||
azp/posix/3 | ||
destructive | ||
needs/root |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
--- | ||
# Copyright (c) Ansible Project | ||
# 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 | ||
|
||
dependencies: | ||
- setup_pkg_mgr | ||
- setup_remote_tmp_dir |
Oops, something went wrong.