Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
a8b95d7
Adding apt-install
sgn4sangar Nov 26, 2025
9c455ff
removing typos
sgn4sangar Nov 26, 2025
d75a232
fixing return type anamoly
sgn4sangar Nov 26, 2025
2bc050d
Incorporating pre-review comments
sgn4sangar Nov 27, 2025
89e6d1c
Update exasol/exaslpm/pkg_mgmt/install_apt.py
sgn4sangar Nov 27, 2025
c966d22
Update exasol/exaslpm/pkg_mgmt/install_apt.py
sgn4sangar Nov 27, 2025
1cc3033
Update exasol/exaslpm/pkg_mgmt/install_apt.py
sgn4sangar Nov 27, 2025
48483e0
Update exasol/exaslpm/pkg_mgmt/install_apt.py
sgn4sangar Nov 27, 2025
2619e7b
Update exasol/exaslpm/pkg_mgmt/install_apt.py
sgn4sangar Nov 27, 2025
d5835ed
Update exasol/exaslpm/pkg_mgmt/install_apt.py
sgn4sangar Nov 27, 2025
bad8041
Update exasol/exaslpm/pkg_mgmt/install_apt.py
sgn4sangar Nov 27, 2025
dd55e7e
Update exasol/exaslpm/pkg_mgmt/install_apt.py
sgn4sangar Nov 27, 2025
ec68ae9
Incorporating CommandExecutor
sgn4sangar Nov 27, 2025
3614b89
A wrapper over CmdExecutor
sgn4sangar Nov 28, 2025
4f4e82b
fixing formatting and linting
sgn4sangar Nov 28, 2025
b4de9cb
minor changes and print function
sgn4sangar Nov 28, 2025
38c6b9b
wait as lambda and minor changes
sgn4sangar Dec 1, 2025
817f03c
Adding unit test for CommandExecutor
sgn4sangar Dec 1, 2025
fdca7bc
formatting
sgn4sangar Dec 1, 2025
2f7eeb5
adding unit test cases for install_via_apt
sgn4sangar Dec 2, 2025
ab0e6ee
incorporating comments
sgn4sangar Dec 2, 2025
dd8495c
reverting back the count-logic
sgn4sangar Dec 2, 2025
415a966
Introducing logger and optimizing test_apt_install
sgn4sangar Dec 2, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 0 additions & 5 deletions exasol/exaslpm/model/package_file_config.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,3 @@
import pathlib
from enum import Enum
from pathlib import Path
from typing import List

from pydantic import (
BaseModel,
field_validator,
Expand Down
100 changes: 100 additions & 0 deletions exasol/exaslpm/pkg_mgmt/cmd_executor.py
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: ...
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
def info(self, msg: str) -> None: ...
def info(self, msg: str, **kwargs) -> None: ...

We probably should add to all methods the kwargs, then we can format the result if we want.

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:
Copy link

Choose a reason for hiding this comment

The 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,
Copy link

Choose a reason for hiding this comment

The 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),
)
69 changes: 69 additions & 0 deletions exasol/exaslpm/pkg_mgmt/install_apt.py
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()
Copy link

Choose a reason for hiding this comment

The 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")
34 changes: 32 additions & 2 deletions exasol/exaslpm/pkg_mgmt/install_packages.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,26 @@
import pathlib

import click
import yaml

from exasol.exaslpm.model.package_file_config import (
PackageFile,
Phase,
)
from exasol.exaslpm.pkg_mgmt.cmd_executor import (
CommandExecutor,
StdLogger,
)
from exasol.exaslpm.pkg_mgmt.install_apt import install_via_apt


def parse_package_file(
package_file: PackageFile, phase_name: str, build_step_name: str
) -> Phase:
build_steps = package_file.build_steps
build_step = build_steps[build_step_name]
phases = build_step.phases
phase = phases[phase_name]
return phase


def package_install(
Expand All @@ -11,6 +31,7 @@ def package_install(
conda_binary: pathlib.Path,
r_binary: pathlib.Path,
):
"""
click.echo(
f"Phase: {phase}, \
Package File: {package_file}, \
Expand All @@ -19,6 +40,15 @@ def package_install(
Conda Binary: {conda_binary}, \
R Binary: {r_binary}",
)
"""

package_content = package_file.read_text()
click.echo(package_content)
try:
logger = StdLogger()
yaml_data = yaml.safe_load(package_content)
pkg_file = PackageFile.model_validate(yaml_data)
single_phase = parse_package_file(pkg_file, phase, build_step)
if single_phase.apt is not None:
install_via_apt(single_phase.apt, CommandExecutor(logger), logger)
except ValueError:
print("Error parsing package file")
6 changes: 3 additions & 3 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,17 @@ homepage = "https://github.com/exasol/script-languages-package-management"
[tool.poetry.dependencies]
click = ">=8.3.0, <9.0.0"
pydantic="^2.10.2"
pyyaml = "^6.0.3"

[tool.poetry.group.dev.dependencies]
exasol-toolbox = ">=1.6.0, <2.0.0"
pyinstaller = ">=6.15.0, <7.0.0"
types-pyinstaller = ">=6.15.0.20250822, <7.0.0.0"
docker = { version = ">=7.1.0,<8.0.0", markers = "sys_platform != 'win32'" }

[tool.poetry.scripts]
exaslpm = 'exasol.exaslpm.main:main'

[build-system]
requires = ["poetry-core>=2.0.0"]
build-backend = "poetry.core.masonry.api"
Expand Down
71 changes: 71 additions & 0 deletions test/resources/package_file_01.yaml
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.
Empty file added test/unit/pkg_mgmt/__init__.py
Empty file.
Loading
Loading