Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
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
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ gl-flavors-parse = "gardenlinux.flavors.__main__:main"
gl-oci = "gardenlinux.oci.__main__:main"
gl-s3 = "gardenlinux.s3.__main__:main"
gl-gh = "gardenlinux.github.__main__:main"
gl-python-exportlibs = "gardenlinux.export_libs.__main__:main"

[tool.pytest.ini_options]
pythonpath = ["src"]
Expand Down
58 changes: 58 additions & 0 deletions src/gardenlinux/export_libs/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

"""
gl-python-exportlibs main entrypoint
"""

from argparse import ArgumentParser

from .exporter import _get_default_package_dir, export

_ARGS_TYPE_ALLOWED = [
"copy",
]


def parse_args():
"""
Parses arguments used for main()

:return: (object) Parsed argparse.ArgumentParser namespace
:since: TODO
"""

parser = ArgumentParser(
description="Export shared libraries required by installed pip packages to a portable directory"
)

parser.add_argument("type", choices=_ARGS_TYPE_ALLOWED, default=None)
parser.add_argument(
"--output-dir",
default="/required_libs",
help="Directory containing the shared libraries.",
)
parser.add_argument(
"--package-dir",
default=_get_default_package_dir(),
help="Path of the generated output",
)

return parser.parse_args()


def main():
"""
gl-python-exportlibs main()

:since: TODO
"""

args = parse_args()

export(output_dir=args.output_dir, package_dir=args.package_dir)


if __name__ == "__main__":
# Create a null logger as default
main()
132 changes: 132 additions & 0 deletions src/gardenlinux/export_libs/exporter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
# -*- coding: utf-8 -*-

import os
import re
import shutil
import subprocess
from os import PathLike

from ..logger import LoggerSetup

# Parses dependencies from ld output
parse_output = re.compile("(?:.*=>)?\\s*(/\\S*).*\n")
# Remove leading /
remove_root = re.compile("^/")


# Check for ELF header
def _isElf(path: str) -> bool:
"""
Checks if a file is an ELF by looking for the ELF header.

:param path: Path to file

:return: (bool) If the file found at path is an ELF
:since: TODO
"""

with open(path, "rb") as f:
return f.read(4) == b"\x7f\x45\x4c\x46"


def _getInterpreter(path: str, logger) -> str:
"""
Returns the interpreter of an ELF. Supported architectures: x86_64, aarch64, i686.

:param path: Path to file
:param logger: Logger to log errors

:return: (str) Path of the interpreter
:since: TODO
"""

with open(path, "rb") as f:
head = f.read(19)

if head[5] == 1:
arch = head[17:]
elif head[5] == 2:
arch = head[17:][::-1]
else:
logger.error(
f"Error: Unknown endianess value for {path}: expected 1 or 2, but was {head[5]}"
)
exit(1)

if arch == b"\x00\xb7": # 00b7: aarch64
return "/lib/ld-linux-aarch64.so.1"
elif arch == b"\x00\x3e": # 003e: x86_64
return "/lib64/ld-linux-x86-64.so.2"
elif arch == b"\x00\x03": # 0003: i686
return "/lib/ld-linux.so.2"
else:
logger.error(
f"Error: Unsupported architecture for {path}: only support x86_64 (003e), aarch64 (00b7) and i686 (0003), but was {arch}"
)
exit(1)


def _get_default_package_dir() -> str:
"""
Finds the default site-packages or dist-packages directory of the default python3 environment

:return: (str) Path to directory
:since: TODO
"""

# Needs to escape the virtual environment python-gardenlinx-lib is running in
out = subprocess.run(
["/bin/sh", "-c", 'python3 -c "import site; print(site.getsitepackages()[0])"'],
stdout=subprocess.PIPE,
)
return out.stdout.decode().strip()


def export(
output_dir: str | PathLike[str] = "/required_libs",
package_dir: str | PathLike[str] = None,
logger=None,
) -> None:
"""
Identifies shared library dependencies of `package_dir` and copies them to `output_dir`.

:param output_dir: Path to output_dir
:param package_dir: Path to package_dir
:param logger: Logger to log errors

:since: TODO
"""

if not package_dir:
package_dir = _get_default_package_dir()
if logger is None or not logger.hasHandlers():
logger = LoggerSetup.get_logger("gardenlinux.export_libs")
# Collect ld dependencies for installed pip packages
dependencies = set()
for root, dirs, files in os.walk(package_dir):
for file in files:
path = f"{root}/{file}"
if not os.path.islink(path) and _isElf(path):
out = subprocess.run(
[_getInterpreter(path, logger), "--inhibit-cache", "--list", path],
stdout=subprocess.PIPE,
)
for dependency in parse_output.findall(out.stdout.decode()):
dependencies.add(os.path.realpath(dependency))

# Copy dependencies into output_dir folder
if not os.path.isdir(output_dir):
os.mkdir(output_dir)

for dependency in dependencies:
path = os.path.join(output_dir, remove_root.sub("", dependency))
os.makedirs(os.path.dirname(path), exist_ok=True)
shutil.copy2(dependency, path)

# Reset timestamps of the parent directories
if len(dependencies) > 0:
mtime = int(os.stat(dependencies.pop()).st_mtime)
os.utime(output_dir, (mtime, mtime))
for root, dirs, _ in os.walk(output_dir):
for dir in dirs:
os.utime(f"{root}/{dir}", (mtime, mtime))
Loading