-
Notifications
You must be signed in to change notification settings - Fork 145
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
add bootc installation method4 #3586
base: package-manager-engines
Are you sure you want to change the base?
Conversation
Hi, @happz. I wanted to ask what the relationship is between |
Ideally, all of it. Package managers govern how packages are installed, providing the primitives. Constructing commands in The issue is, they do run their commands immediately. How about we add a middleman, an "adapter" class, which would take command from a package manager, and do something with it? The default one, the current behavior, would be to take that command and run it on the guest, but your TL;DR: we want those commands, we just don't want them to run on the guest now. We need to tell package managers not to run them, instead we need to collect them and add them to the containerfile. Eventually, your self.guest.package_manager.install(
*self.list_installables("package", *self.packages),
options=Options(
excluded_packages=self.exclude,
skip_missing=self.skip_missing,
),
) And the output would not be I was thinking about something like this but I didn't have a chance to do it properly (or everywhere), and there are some typing issues that need to be sorted out, plus actions that run more than one command (if there are any, and there might be, in diff --git a/tmt/package_managers/__init__.py b/tmt/package_managers/__init__.py
index 829e33aa..f17ca6d8 100644
--- a/tmt/package_managers/__init__.py
+++ b/tmt/package_managers/__init__.py
@@ -1,13 +1,13 @@
import shlex
from collections.abc import Iterator
-from typing import TYPE_CHECKING, Any, Callable, Optional, Union
+from typing import TYPE_CHECKING, Any, Callable, Optional, Union, TypeVar, Generic
import tmt
import tmt.log
import tmt.plugins
import tmt.utils
from tmt.container import container, simple_field
-from tmt.utils import Command, CommandOutput, Path
+from tmt.utils import Command, CommandOutput, Path, ShellScript
if TYPE_CHECKING:
from tmt._compat.typing import TypeAlias
@@ -16,6 +16,8 @@ if TYPE_CHECKING:
#: A type of package manager names.
GuestPackageManager: TypeAlias = str
+T = TypeVar('T')
+
#
# Installable objects
@@ -148,6 +150,29 @@ class Options:
allow_untrusted: bool = False
+class CommandAdapter(Generic[T]):
+ def on_command(self, script: ShellScript) -> T:
+ raise NotImplementedError
+
+
+class RunImmediatelyAdapter(CommandAdapter[CommandOutput]):
+ def __init__(self, guest: 'Guest', logger: tmt.log.Logger) -> None:
+ self.logger = logger
+ self.guest = guest
+
+ def on_command(self, script):
+ return self.guest.execute(script)
+
+
+class CollectAdapter(CommandAdapter[ShellScript]):
+ def __init__(self, guest: 'Guest', logger: tmt.log.Logger) -> None:
+ self.logger = logger
+ self.guest = guest
+
+ def on_command(self, script):
+ return script
+
+
class PackageManager(tmt.utils.Common):
"""
A base class for package manager plugins
@@ -169,12 +194,16 @@ class PackageManager(tmt.utils.Common):
command: Command
options: Command
- def __init__(self, *, guest: 'Guest', logger: tmt.log.Logger) -> None:
+ adapter: CommandAdapter[T]
+
+ def __init__(self, *, guest: 'Guest', adapter: CommandAdapter[T], logger: tmt.log.Logger) -> None:
super().__init__(logger=logger)
self.guest = guest
self.command, self.options = self.prepare_command()
+ self.adapter = adapter or RunImmediatelyAdapter(guest, logger)
+
def prepare_command(self) -> tuple[Command, Command]:
"""
Prepare installation command and subcommand options
@@ -193,22 +222,22 @@ class PackageManager(tmt.utils.Common):
self,
*installables: Installable,
options: Optional[Options] = None,
- ) -> CommandOutput:
+ ) -> T:
raise NotImplementedError
def reinstall(
self,
*installables: Installable,
options: Optional[Options] = None,
- ) -> CommandOutput:
+ ) -> T:
raise NotImplementedError
def install_debuginfo(
self,
*installables: Installable,
options: Optional[Options] = None,
- ) -> CommandOutput:
+ ) -> T:
raise NotImplementedError
- def refresh_metadata(self) -> CommandOutput:
+ def refresh_metadata(self) -> T:
raise NotImplementedError
diff --git a/tmt/package_managers/dnf.py b/tmt/package_managers/dnf.py
index cb7612f2..bc113985 100644
--- a/tmt/package_managers/dnf.py
+++ b/tmt/package_managers/dnf.py
@@ -160,14 +160,14 @@ class Dnf(tmt.package_managers.PackageManager):
f'{self.command.to_script()} makecache {self.options.to_script()} --refresh'
)
- return self.guest.execute(script)
+ return self.adapter.on_command(script)
def install(
self,
*installables: Installable,
options: Optional[Options] = None,
) -> CommandOutput:
- return self.guest.execute(self._construct_install_script(*installables, options=options))
+ return self.adapter.on_command(self._construct_install_script(*installables, options=options))
def reinstall(
self,
diff --git a/tmt/steps/provision/__init__.py b/tmt/steps/provision/__init__.py
index 6bc6e60a..692335f6 100644
--- a/tmt/steps/provision/__init__.py
+++ b/tmt/steps/provision/__init__.py
@@ -1095,7 +1095,7 @@ class Guest(tmt.utils.Common):
)
return tmt.package_managers.find_package_manager(self.facts.package_manager)(
- guest=self, logger=self._logger
+ guest=self, logger=self._logger, adapter=RunImmediatelyAdapter(guest, logger)
)
@functools.cached_property |
@happz, the idea with adapters sounds great to me as I was struggling to come up with a solution on how not to execute commands immediately. I'll try to move all logic to the package manager next week, thank you! |
Sounds good, but it didn't work. I'm not smart enough to appease all type-related issues I was hitting. So, I took different approach: #3587 - not tested, just locally... Package manager we have are now split into two parts: "engine" which does what PM does today, prepares commands to install stuff, but it does not run anything - instead, it returns these commands. The rest of code then works with "package manager" part, that is just a "call the engine and run what it gives you" frontend for its engine. tmt still uses this interface, calls And for your use case, I imagined your code could take the engine itself, e.g. Something like this: # Let's say we collect instructions for the final Containerfile in self._containerfile_directives, and that we have self.engine which is the DnfEngine
def install_from_repository(self) -> None:
script = self.engine.install(
*self.list_installables("package", *self.packages),
options=Options(
excluded_packages=self.exclude,
skip_missing=self.skip_missing,
),
)
self._containerfile_directives.append(f'RUN {script}') Feel free to play with it, I didn't experiment with your patch yet. Also, all those Plus, you will not need an extra flag for tracking whether there are any commands to include - if Let me know about your progress or any idea you get about the solution. |
e1f91a4
to
5a22cc5
Compare
Pull Request Checklist