-
Notifications
You must be signed in to change notification settings - Fork 0
#7: Implemented apt install command #24
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
base: main
Are you sure you want to change the base?
Changes from all commits
a8b95d7
9c455ff
d75a232
2bc050d
89e6d1c
c966d22
1cc3033
48483e0
2619e7b
d5835ed
bad8041
dd55e7e
ec68ae9
3614b89
4f4e82b
b4de9cb
38c6b9b
817f03c
fdca7bc
2f7eeb5
ab0e6ee
dd8495c
415a966
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,100 @@ | ||
| import subprocess | ||
| import sys | ||
| from collections.abc import ( | ||
| Callable, | ||
| Iterator, | ||
| ) | ||
| from typing import Protocol | ||
|
|
||
|
|
||
| class CommandLogger(Protocol): | ||
| def info(self, msg: str) -> None: ... | ||
| def warn(self, msg: str) -> None: ... | ||
| def error(self, msg: str) -> None: ... | ||
|
|
||
|
|
||
| class StdLogger: | ||
| def info(self, msg: str) -> None: | ||
| self._last_msg = msg | ||
| sys.stdout.write(msg) | ||
|
|
||
| def warn(self, msg: str) -> None: | ||
| self._last_msg = msg | ||
| sys.stderr.write(msg) | ||
|
|
||
| def error(self, msg: str) -> None: | ||
| self._last_msg = msg | ||
| sys.stderr.write(msg) | ||
|
|
||
| def get_last_msg(self) -> str: | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why do we need this? |
||
| return self._last_msg | ||
|
|
||
|
|
||
| class CommandResult: | ||
| def __init__( | ||
| self, | ||
| logger: CommandLogger, | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would move this to the end of the parameter list |
||
| fn_ret_code: Callable[[], int], | ||
| stdout: Iterator[str], | ||
| stderr: Iterator[str], | ||
| ): | ||
| self._log = logger | ||
| self._fn_return_code = fn_ret_code # a lambda to subprocess.open.wait | ||
| self._stdout = stdout | ||
| self._stderr = stderr | ||
|
|
||
| def return_code(self): | ||
| return self._fn_return_code() | ||
|
|
||
| def itr_stdout(self) -> Iterator[str]: | ||
| return self._stdout | ||
|
|
||
| def itr_stderr(self) -> Iterator[str]: | ||
| return self._stderr | ||
|
|
||
| def consume_results( | ||
| self, | ||
| consume_stdout: Callable[[str | bytes], None], | ||
| consume_stderr: Callable[[str | bytes], None], | ||
| ): | ||
|
|
||
| def pick_next(out_stream, callback) -> bool: | ||
| try: | ||
| _val = next(out_stream) | ||
| callback(_val) | ||
| except StopIteration: | ||
| return False | ||
| return True | ||
|
|
||
| # Read from _stdout and _stderr simultaneously | ||
| stdout_continue = True | ||
| stderr_continue = True | ||
| while stdout_continue or stderr_continue: | ||
| if stdout_continue: | ||
| stdout_continue = pick_next(self._stdout, consume_stdout) | ||
| if stderr_continue: | ||
| stderr_continue = pick_next(self._stderr, consume_stderr) | ||
| return self.return_code() | ||
|
|
||
| def print_results(self): | ||
| ret_code = self.consume_results(self._log.info, self._log.error) | ||
| self._log.info(f"Return Code: {ret_code}") | ||
|
|
||
|
|
||
| class CommandExecutor: | ||
| def __init__(self, logger: CommandLogger): | ||
| self._log = logger | ||
|
|
||
| def execute(self, cmd_strs: list[str]) -> CommandResult: | ||
| cmd_str = " ".join(cmd_strs) | ||
| self._log.info(f"Executing: {cmd_str}") | ||
|
|
||
| sub_process = subprocess.Popen( | ||
| cmd_strs, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True | ||
| ) | ||
| return CommandResult( | ||
| self._log, | ||
| fn_ret_code=lambda: sub_process.wait(), | ||
| stdout=iter(sub_process.stdout), | ||
| stderr=iter(sub_process.stderr), | ||
| ) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,69 @@ | ||
| from exasol.exaslpm.model.package_file_config import AptPackages | ||
| from exasol.exaslpm.pkg_mgmt.cmd_executor import ( | ||
| CommandExecutor, | ||
| CommandLogger, | ||
| ) | ||
|
|
||
|
|
||
| def prepare_update_command() -> list[str]: | ||
| update_cmd = ["apt-get", "-y", "update"] | ||
| return update_cmd | ||
|
|
||
|
|
||
| def prepare_clean_cmd() -> list[str]: | ||
| clean_cmd = ["apt-get", "-y", "clean"] | ||
| return clean_cmd | ||
|
|
||
|
|
||
| def prepare_autoremove_cmd() -> list[str]: | ||
| autoremove_cmd = ["apt-get", "-y", "autoremove"] | ||
| return autoremove_cmd | ||
|
|
||
|
|
||
| def prepare_ldconfig_cmd() -> list[str]: | ||
| ldconfig_cmd = ["ldconfig"] | ||
| return ldconfig_cmd | ||
|
|
||
|
|
||
| def prepare_locale_cmd() -> list[str]: | ||
| locale_cmd = ["locale-gen", "&&", "update-locale", "LANG=en_US.UTF8"] | ||
| return locale_cmd | ||
|
|
||
|
|
||
| def prepare_install_cmd(apt_packages: AptPackages) -> list[str]: | ||
| install_cmd = ["apt-get", "install", "-V", "-y", "--no-install-recommends"] | ||
| if apt_packages.packages is not None: | ||
| for package in apt_packages.packages: | ||
| install_cmd.append(f"{package.name}={package.version}") | ||
| return install_cmd | ||
|
|
||
|
|
||
| def install_via_apt( | ||
| apt_packages: AptPackages, executor: CommandExecutor, log: CommandLogger | ||
| ): | ||
| if len(apt_packages.packages) > 0: | ||
| update_cmd = prepare_update_command() | ||
| cmd_res = executor.execute(update_cmd) | ||
| cmd_res.print_results() | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. After we printed the results, we need to check the return_code if the command was successful. This is needed for each command. |
||
|
|
||
| install_cmd = prepare_install_cmd(apt_packages) | ||
| cmd_res = executor.execute(install_cmd) | ||
| cmd_res.print_results() | ||
|
|
||
| clean_cmd = prepare_clean_cmd() | ||
| cmd_res = executor.execute(clean_cmd) | ||
| cmd_res.print_results() | ||
|
|
||
| autoremove_cmd = prepare_autoremove_cmd() | ||
| cmd_res = executor.execute(autoremove_cmd) | ||
| cmd_res.print_results() | ||
|
|
||
| locale_cmd = prepare_locale_cmd() | ||
| cmd_res = executor.execute(locale_cmd) | ||
| cmd_res.print_results() | ||
|
|
||
| ldconfig_cmd = prepare_ldconfig_cmd() | ||
| cmd_res = executor.execute(ldconfig_cmd) | ||
| cmd_res.print_results() | ||
| else: | ||
| log.error("Got an empty list of AptPackages") | ||
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,71 @@ | ||
| # Top-level comment about the package file | ||
| comment: "Main package file for build steps" | ||
|
|
||
| build_steps: | ||
| build_step_1: | ||
| comment: "First build step, e.g., system dependencies" | ||
| phases: | ||
| phase_1: | ||
| comment: "APT packages for base system" | ||
| apt: | ||
| comment: "Comment for APT section" | ||
| packages: | ||
| - name: "curl" | ||
| version: "7.68.0" | ||
| comment: "For downloading" | ||
| - name: "git" | ||
| version: "2.25.1" | ||
| comment: "VCS support" | ||
| pip: | ||
| comment: "Python packages" | ||
| packages: | ||
| - name: "requests" | ||
| version: "2.31.0" | ||
| comment: "HTTP package" | ||
| - name: "click" | ||
| version: "8.1.7" | ||
| comment: "CLI utilities" | ||
| r: | ||
| comment: "Optional R packages" | ||
| packages: | ||
| - name: "ggplot2" | ||
| version: "3.4.0" | ||
| comment: "Plotting library" | ||
| conda: | ||
| comment: "Optional conda packages" | ||
| channels: | ||
| - "defaults" | ||
| - "conda-forge" | ||
| packages: | ||
| - name: "numpy" | ||
| version: "1.26.0" | ||
| comment: "NumPy for Python" | ||
| - name: "pandas" | ||
| version: "2.1.2" | ||
| comment: "Data manipulation" | ||
| phase_2: | ||
| comment: "Dev tools for CI/CD" | ||
| apt: | ||
| comment: "Additional apt packages" | ||
| packages: | ||
| - name: "python3" | ||
| version: "3.8.10" | ||
| comment: "System python" | ||
| # You can leave out pip/r/conda if not needed in this phase | ||
|
|
||
| build_step_2: | ||
| comment: "Second build step, e.g., App build" | ||
| phases: | ||
| phase_install: | ||
| comment: "Install app dependencies" | ||
| pip: | ||
| packages: | ||
| - name: "flask" | ||
| version: "3.0.0" | ||
| comment: "Web framework" | ||
| - name: "sqlalchemy" | ||
| version: "2.0.22" | ||
| comment: "ORM" | ||
| # APT/R/Conda can be omitted if not needed here | ||
|
|
||
| # You can add further build_steps and phases as needed. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We probably should add to all methods the kwargs, then we can format the result if we want.