-
Notifications
You must be signed in to change notification settings - Fork 1
feat: Port exportLibs.py script to python-gardenlinux-lib
#253
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
Open
Leon-hk
wants to merge
18
commits into
main
Choose a base branch
from
feat/export-libs
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
18 commits
Select commit
Hold shift + click to select a range
9e14a1d
feat: add exportLibs feature to python-gardenlinux-lib
Leon-hk 60c71a4
move export-libs to gardenlinux.build and use pyelftools to retrieve …
Leon-hk fd22637
revert formatting change of unrelated feature
Leon-hk 52b48e0
Merge branch 'main' into feat/export-libs
Leon-hk 2e2f289
Identify ELF files using pyelftools
Leon-hk e5cab98
use pathlib.Path and retrieve primary python interpreter directly fro…
Leon-hk ba84e47
replace os.path functions with pathlib functions
Leon-hk c1e6377
updpate .gitignore to only exclude build folder at root level
Leon-hk bd780c6
replace logger infos with exceptions
Leon-hk cdc6983
directly raise RuntimeError instead of returning None
Leon-hk 38fa08c
fix: remove duplicated python3 in path
Leon-hk 389bf5c
insert 'since' value 1.0.0
Leon-hk b3b1e80
replace PATH readout with shutil.which()
Leon-hk c22d2d3
Merge branch 'main' into feat/export-libs
Leon-hk 7c11b65
add test for exporter
Leon-hk 26fc527
Merge branch 'main' into feat/export-libs
Leon-hk a219fac
enhance control flow
Leon-hk 2418ffb
minor cleanups
Leon-hk File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -11,7 +11,7 @@ __pycache__/ | |
|
|
||
| # Distribution / packaging | ||
| .Python | ||
| build/ | ||
| /build/ | ||
| develop-eggs/ | ||
| dist/ | ||
| downloads/ | ||
|
|
||
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,66 @@ | ||
| #!/usr/bin/env python3 | ||
| # -*- coding: utf-8 -*- | ||
|
|
||
| """ | ||
| gl-build main entrypoint | ||
| """ | ||
|
|
||
| import pathlib | ||
| from argparse import ArgumentParser | ||
|
|
||
| from .exporter import _get_default_package_dir, export | ||
|
|
||
| _ARGS_TYPE_ALLOWED = [ | ||
| "export-python-libs", | ||
| ] | ||
|
|
||
|
|
||
| def parse_args(): | ||
| """ | ||
| Parses arguments used for main() | ||
|
|
||
| :return: (object) Parsed argparse.ArgumentParser namespace | ||
| :since: 1.0.0 | ||
| """ | ||
|
|
||
| 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-gl-build main() | ||
|
|
||
| :since: 1.0.0 | ||
| """ | ||
|
|
||
| args = parse_args() | ||
|
|
||
| match args.type: | ||
| case "export-python-libs": | ||
| export( | ||
| output_dir=pathlib.Path(args.output_dir), | ||
| package_dir=pathlib.Path(args.package_dir), | ||
| ) | ||
| case _: | ||
| raise NotImplementedError | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| # Create a null logger as default | ||
| main() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,138 @@ | ||
| # -*- coding: utf-8 -*- | ||
|
|
||
| import os | ||
| import pathlib | ||
| import re | ||
| import shutil | ||
| import subprocess | ||
| from os import PathLike | ||
|
|
||
| from elftools.common.exceptions import ELFError | ||
| from elftools.elf.elffile import ELFFile | ||
|
|
||
| # 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 | PathLike[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: 1.0.0 | ||
| """ | ||
|
|
||
| with open(path, "rb") as f: | ||
| try: | ||
| ELFFile(f) | ||
| return True | ||
| except ELFError: | ||
| return False | ||
|
|
||
|
|
||
| def _getInterpreter(path: str | PathLike[str]) -> pathlib.Path: | ||
| """ | ||
| Returns the interpreter of an ELF. Supported architectures: x86_64, aarch64, i686. | ||
|
|
||
| :param path: Path to file | ||
|
|
||
| :return: (str) Path of the interpreter | ||
| :since: 1.0.0 | ||
| """ | ||
|
|
||
| with open(path, "rb") as f: | ||
| elf = ELFFile(f) | ||
| interp = elf.get_section_by_name(".interp") | ||
|
|
||
| if interp: | ||
| return pathlib.Path(interp.data().split(b"\x00")[0].decode()) | ||
| else: | ||
| match elf.header["e_machine"]: | ||
| case "EM_AARCH64": | ||
| return pathlib.Path("/lib/ld-linux-aarch64.so.1") | ||
| case "EM_386": | ||
| return pathlib.Path("/lib/ld-linux.so.2") | ||
| case "EM_X86_64": | ||
| return pathlib.Path("/lib64/ld-linux-x86-64.so.2") | ||
| case arch: | ||
| raise RuntimeError( | ||
| f"Error: Unsupported architecture for {path}: only support x86_64 (003e), aarch64 (00b7) and i686 (0003), but was {arch}" | ||
| ) | ||
|
|
||
|
|
||
| def _get_default_package_dir() -> pathlib.Path: | ||
| """ | ||
| Finds the default site-packages or dist-packages directory of the default python3 environment | ||
|
|
||
| :return: (str) Path to directory | ||
| :since: 1.0.0 | ||
| """ | ||
|
|
||
| # Needs to escape the virtual environment python-gardenlinx-lib is running in | ||
| interpreter = shutil.which("python3") | ||
|
|
||
| if not interpreter: | ||
| raise RuntimeError( | ||
| f"Error: Couldn't identify a default python package directory. Please specifiy one using the --package-dir option. Use -h for more information." | ||
| ) | ||
|
|
||
| out = subprocess.run( | ||
| [interpreter, "-c", "import site; print(site.getsitepackages()[0])"], | ||
| stdout=subprocess.PIPE, | ||
| ) | ||
| return pathlib.Path(out.stdout.decode().strip()) | ||
|
|
||
|
|
||
| def export( | ||
| output_dir: str | PathLike[str] = "/required_libs", | ||
| package_dir: str | PathLike[str] | None = 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 | ||
|
|
||
| :since: 1.0.0 | ||
| """ | ||
|
|
||
| if not package_dir: | ||
| package_dir = _get_default_package_dir() | ||
| else: | ||
| package_dir = pathlib.Path(package_dir) | ||
| output_dir = pathlib.Path(output_dir) | ||
|
|
||
| # Collect ld dependencies for installed pip packages | ||
| dependencies = set() | ||
| for root, dirs, files in package_dir.walk(): | ||
| for file in files: | ||
| path = root.joinpath(file) | ||
| if not path.is_symlink() and _isElf(path): | ||
| out = subprocess.run( | ||
| [_getInterpreter(path), "--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 output_dir.is_dir(): | ||
| output_dir.mkdir() | ||
|
|
||
| for dependency in dependencies: | ||
| path = output_dir.joinpath(remove_root.sub("", dependency)) | ||
| path.parent.mkdir(parents=True, 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 output_dir.walk(): | ||
| for dir in dirs: | ||
| os.utime(root.joinpath(dir), (mtime, mtime)) | ||
Empty file.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| import pathlib | ||
| import shutil | ||
| import subprocess | ||
| import tempfile | ||
|
|
||
| from gardenlinux.build.exporter import export | ||
|
|
||
| arm64 = {'usr', 'usr/lib', 'usr/lib/aarch64-linux-gnu', 'usr/lib/aarch64-linux-gnu/ld-linux-aarch64.so.1', 'usr/lib/aarch64-linux-gnu/libc.so.6', 'usr/lib/aarch64-linux-gnu/libpthread.so.0'} | ||
| amd64 = {'usr', 'usr/lib', 'usr/lib/x86_64-linux-gnu', 'usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2', 'usr/lib/x86_64-linux-gnu/libc.so.6', 'usr/lib/x86_64-linux-gnu/libpthread.so.0'} | ||
|
|
||
| def test_requests_export(): | ||
| dpkg = shutil.which("dpkg") | ||
| assert dpkg | ||
|
|
||
| out = subprocess.run([dpkg, "--print-architecture"], stdout=subprocess.PIPE) | ||
| assert out.returncode == 0 | ||
| arch = out.stdout.decode().strip() | ||
|
|
||
| with tempfile.TemporaryDirectory() as target_dir: | ||
| target_dir = pathlib.Path(target_dir) | ||
| site_packages = target_dir.joinpath("site-packages") | ||
| required_libs = target_dir.joinpath("required_libs") | ||
|
|
||
| site_packages.mkdir() | ||
| required_libs.mkdir() | ||
|
|
||
| pip3 = shutil.which("pip3") | ||
| assert pip3 | ||
|
|
||
| out = subprocess.run([pip3, "install", "--target", site_packages, "requests"]) | ||
|
|
||
| assert out.returncode == 0 | ||
|
|
||
| export(required_libs, site_packages) | ||
|
|
||
| exported = set() | ||
|
|
||
| for path in required_libs.rglob("*"): | ||
| exported.add(str(path.relative_to(required_libs))) | ||
|
|
||
| if arch == "arm64": | ||
| expected = arm64 | ||
| elif arch == "amd64": | ||
| expected = amd64 | ||
| else: | ||
| raise NotImplementedError(f"Architecture {arch} not supported") | ||
|
|
||
| assert exported == expected |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.