Skip to content

Commit

Permalink
Merge pull request #464 from zapta/develop
Browse files Browse the repository at this point in the history
Added handling of broken package installations.
  • Loading branch information
Obijuan authored Nov 17, 2024
2 parents a9cc221 + 461f73a commit f0aca51
Show file tree
Hide file tree
Showing 6 changed files with 368 additions and 56 deletions.
178 changes: 132 additions & 46 deletions apio/commands/packages.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,117 @@
"""Implementation of 'apio packages' command"""

from pathlib import Path
from typing import Tuple
from typing import Tuple, List
from varname import nameof
import click
from click.core import Context
from apio.managers import installer
from apio.resources import Resources
from apio import cmd_util
from apio import cmd_util, pkg_util, util
from apio.commands import options


def _install(
resources: Resources, packages: List[str], force: bool, verbose: bool
) -> int:
"""Handles the --install operation. Returns exit code."""
click.secho(f"Platform id '{resources.platform_id}'")

# -- If packages where specified, install all packages that are valid
# -- for this platform.
if not packages:
packages = resources.platform_packages.keys()

# -- Install the packages, one by one.
for package in packages:
installer.install_package(
resources, package_spec=package, force=force, verbose=verbose
)

return 0


def _uninstall(
resources: Resources, packages: List[str], verbose: bool, sayyes: bool
) -> int:
"""Handles the --uninstall operation. Returns exit code."""

# -- If packages where specified, uninstall all packages that are valid
# -- for this platform.
if not packages:
packages = resources.platform_packages.keys()

# -- Ask the user for confirmation
if not (
sayyes
or click.confirm(
"Do you want to uninstall " f"{util.count(packages, 'package')}?"
)
):
# -- User doesn't want to continue.
click.secho("User said no", fg="red")
return 1

# -- Here when going on with the uninstallation.
click.secho(f"Platform id '{resources.platform_id}'")

# -- Uninstall the packages, one by one
for package in packages:
installer.uninstall_package(
resources, package_spec=package, verbose=verbose
)

return 0


def _fix(resources: Resources, verbose: bool) -> int:
"""Handles the --fix operation. Returns exit code."""

# -- Scan the availeable and installed packages.
scan = pkg_util.scan_packages(resources)

# -- Fix any errors.
if scan.num_errors():
installer.fix_packages(resources, scan, verbose)
else:
click.secho("No errors to fix")

# -- Show the new state
new_scan = pkg_util.scan_packages(resources)
pkg_util.list_packages(resources, new_scan)

return 0


def _list(resources: Resources, verbose: bool) -> int:
"""Handles the --list operation. Returns exit code."""

if verbose:
click.secho(f"Platform id '{resources.platform_id}'")

# -- Scan the available and installed packages.
scan = pkg_util.scan_packages(resources)

# -- List the findings.
pkg_util.list_packages(resources, scan)

# -- Print an hint or summary based on the findings.
if scan.num_errors():
click.secho(
"[Hint] run 'apio packages -fix' to fix the errors.", fg="yellow"
)
elif scan.uninstalled_package_ids:
click.secho(
"[Hint] run 'apio packages -install' to install all "
"available packages.",
fg="yellow",
)
else:
click.secho("All available messages are installed.", fg="green")

return 0


# ---------------------------
# -- COMMAND
# ---------------------------
Expand All @@ -38,6 +139,7 @@
apio packages --install [email protected] # Install a specific version.
apio packages --uninstall # Uninstall all packages.
apio packages --uninstall oss-cad-suite # Uninstall only given package(s).
apio packages --fix # Fix package errors.
Adding --force to --install forces the reinstallation of existing packages,
otherwise, packages that are already installed correctly are left with no
Expand All @@ -48,7 +150,7 @@
"""

install_option = click.option(
"install", # Var name. Deconflicting from Python'g builtin 'all'.
"install", # Var name.
"-i",
"--install",
is_flag=True,
Expand All @@ -57,14 +159,22 @@
)

uninstall_option = click.option(
"uninstall", # Var name. Deconflicting from Python'g builtin 'all'.
"uninstall", # Var name.
"-u",
"--uninstall",
is_flag=True,
help="Uninstall packages.",
cls=cmd_util.ApioOption,
)

fix_option = click.option(
"fix", # Var name.
"--fix",
is_flag=True,
help="Fix package errors.",
cls=cmd_util.ApioOption,
)


# pylint: disable=duplicate-code
# pylint: disable=too-many-arguments
Expand All @@ -80,6 +190,7 @@
@options.list_option_gen(help="List packages.")
@install_option
@uninstall_option
@fix_option
@options.force_option_gen(help="Force installation.")
@options.project_dir_option
@options.platform_option
Expand All @@ -91,8 +202,9 @@ def cli(
packages: Tuple[str],
# Options
list_: bool,
install,
uninstall,
install: bool,
uninstall: bool,
fix: bool,
force: bool,
platform: str,
project_dir: Path,
Expand All @@ -104,10 +216,14 @@ def cli(
"""

# Validate the option combination.
cmd_util.check_exactly_one_param(ctx, nameof(list_, install, uninstall))
cmd_util.check_exactly_one_param(
ctx, nameof(list_, install, uninstall, fix)
)
cmd_util.check_at_most_one_param(ctx, nameof(list_, force))
cmd_util.check_at_most_one_param(ctx, nameof(uninstall, force))
cmd_util.check_at_most_one_param(ctx, nameof(fix, force))
cmd_util.check_at_most_one_param(ctx, nameof(list_, packages))
cmd_util.check_at_most_one_param(ctx, nameof(fix, packages))

# -- Load the resources. We don't care about project specific resources.
resources = Resources(
Expand All @@ -117,47 +233,17 @@ def cli(
)

if install:
click.secho(f"Platform id '{resources.platform_id}'")

# -- If packages not specified, use all.
if not packages:
packages = resources.platform_packages.keys()
# -- Install the packages.
for package in packages:
installer.install_package(
resources, package_spec=package, force=force, verbose=verbose
)

ctx.exit(0)
exit_code = _install(resources, packages, force, verbose)
ctx.exit(exit_code)

if uninstall:
# -- If packages not specified, use all.
if not packages:
packages = resources.platform_packages.keys()
exit_code = _uninstall(resources, packages, verbose, sayyes)
ctx.exit(exit_code)

# -- Ask the user for confirmation
num_packages = (
"1 package" if len(packages) == 1 else f"{len(packages)} packages"
)
if sayyes or click.confirm(
f"Do you want to uninstall {num_packages}?"
):

click.secho(f"Platform id '{resources.platform_id}'")

# -- Uninstall packages, one by one
for package in packages:
installer.uninstall_package(
resources, package_spec=package, verbose=verbose
)

# -- User quit!
else:
click.secho("User said no", fg="red")
ctx.exit(0)
if fix:
exit_code = _fix(resources, verbose)
ctx.exit(exit_code)

# -- Here it must be --list.
if verbose:
click.secho(f"Platform id '{resources.platform_id}'")
resources.list_packages()
ctx.exit(0)
exit_code = _list(resources, verbose)
ctx.exit(exit_code)
45 changes: 41 additions & 4 deletions apio/managers/installer.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
import shutil
import click
import requests
from apio import util
from apio import util, pkg_util
from apio.resources import Resources
from apio.managers.downloader import FileDownloader
from apio.managers.unpacker import FileUnpacker
Expand Down Expand Up @@ -208,20 +208,20 @@ def _parse_package_spec(package_spec: str) -> Tuple[str, str]:


def _delete_package_dir(
resources: Resources, package_name: str, verbose: bool
resources: Resources, package_id: str, verbose: bool
) -> bool:
"""Delete the directory of the package with given name. Returns
True if the packages existed. Exits with an error message on error."""

package_dir = resources.get_package_dir(package_name)
package_dir = resources.get_package_dir(package_id)

dir_found = package_dir.is_dir()
if dir_found:
if verbose:
click.secho(f"Deleting {str(package_dir)}")

# -- Sanity check the path and delete.
package_folder_name = resources.get_package_folder_name(package_name)
package_folder_name = resources.get_package_folder_name(package_id)
assert package_folder_name in str(package_dir), package_dir
shutil.rmtree(package_dir)

Expand Down Expand Up @@ -396,3 +396,40 @@ def uninstall_package(
else:
# -- Package not installed. We treat it as a success.
click.secho(f"Package '{package_name}' was not installed", fg="green")


def fix_packages(
resources: Resources, scan: pkg_util.PackageScanResults, verbose: bool
) -> None:
"""If the package scan result contains errors, fix them."""

# -- If non verbose, print a summary message.
if not verbose:
click.secho(
f"Fixing {util.count(scan.num_errors(), 'package error')}."
)

# -- Fix broken packages.
for package_id in scan.broken_package_ids:
if verbose:
print(f"Uninstalling broken package '{package_id}'")
_delete_package_dir(resources, package_id, verbose=False)
resources.profile.remove_package(package_id)
resources.profile.save()

for package_id in scan.orphan_package_ids:
if verbose:
print(f"Uninstalling unknown package '{package_id}'")
resources.profile.remove_package(package_id)
resources.profile.save()

for dir_name in scan.orphan_dir_names:
if verbose:
print(f"Deleting unknown dir '{dir_name}'")
shutil.rmtree(util.get_packages_dir() / dir_name)

for file_name in scan.orphan_file_names:
if verbose:
print(f"Deleting unknown file '{file_name}'")
file_path = util.get_packages_dir() / file_name
file_path.unlink()
Loading

0 comments on commit f0aca51

Please sign in to comment.