├── tests ├── __init__.py └── test_cli.py ├── shrinkwrap ├── __init__.py ├── ldsoconf.py ├── cli.py └── elf.py ├── .gitignore ├── .flake8 ├── scripts └── upload-to-cache ├── derivation.nix ├── Makefile ├── .github └── workflows │ ├── main.yml │ └── pull_request.yml ├── pyproject.toml ├── LICENSE ├── overlay.nix ├── flake.lock ├── flake.nix ├── README.md └── poetry.lock /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /shrinkwrap/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | */__pycache__ 2 | result 3 | *_stamped 4 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 88 3 | extend-ignore = E203 4 | exclude = 5 | .git, 6 | __pycache__, 7 | *.egg-info, 8 | .nox, 9 | .pytest_cache, 10 | .mypy_cache 11 | result -------------------------------------------------------------------------------- /scripts/upload-to-cache: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | nix-store -qR \ 4 | --include-outputs \ 5 | $(nix-store -qd $(nix build --json | jq -r '.[].outputs | to_entries[].value')) \ 6 | | grep -v '\.drv$' \ 7 | | cachix push fzakaria 8 | -------------------------------------------------------------------------------- /derivation.nix: -------------------------------------------------------------------------------- 1 | { stdenv 2 | , lib 3 | , poetry2nix 4 | , python39 5 | , poetryOverrides 6 | , writeScriptBin 7 | , makeWrapper 8 | }: 9 | poetry2nix.mkPoetryApplication { 10 | projectDir = ./.; 11 | python = python39; 12 | overrides = [ poetry2nix.defaultPoetryOverrides poetryOverrides ]; 13 | } 14 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | lint: 2 | black --check . 3 | isort -c . 4 | flake8 . 5 | 6 | format: 7 | black . 8 | isort . 9 | 10 | typecheck: 11 | mypy --show-error-codes --pretty . 12 | 13 | test: 14 | pytest 15 | 16 | all: lint typecheck test 17 | 18 | clean: 19 | rm -f *_stamped 20 | 21 | .PHONY: typecheck lint format 22 | 23 | .DEFAULT_GOAL := all 24 | -------------------------------------------------------------------------------- /tests/test_cli.py: -------------------------------------------------------------------------------- 1 | from click.testing import CliRunner 2 | 3 | from shrinkwrap import cli 4 | 5 | 6 | def test_cli_no_arguments(): 7 | runner = CliRunner() 8 | result = runner.invoke(cli.shrinkwrap) 9 | assert result.exit_code == 2 10 | 11 | 12 | def test_cli_help(): 13 | runner = CliRunner() 14 | result = runner.invoke(cli.shrinkwrap, ["--help"]) 15 | assert result.exit_code == 0 16 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Main 2 | on: 3 | push: 4 | branches: 5 | - main 6 | jobs: 7 | build: 8 | name: Build 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - uses: actions/checkout@v3 13 | - uses: cachix/install-nix-action@v20 14 | with: 15 | nix_path: nixpkgs=channel:nixos-unstable 16 | - uses: cachix/cachix-action@v12 17 | with: 18 | name: fzakaria 19 | authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}' 20 | - run: nix flake check --print-build-logs 21 | - run: nix build --print-build-logs 22 | - run: nix run . -- --help 23 | -------------------------------------------------------------------------------- /.github/workflows/pull_request.yml: -------------------------------------------------------------------------------- 1 | name: Pull Request 2 | on: 3 | pull_request: 4 | branches: 5 | - main 6 | jobs: 7 | build: 8 | name: Build 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - uses: actions/checkout@v3 13 | - uses: cachix/install-nix-action@v20 14 | with: 15 | nix_path: nixpkgs=channel:nixos-unstable 16 | - uses: cachix/cachix-action@v12 17 | with: 18 | name: fzakaria 19 | authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}' 20 | - run: nix flake check --print-build-logs 21 | - run: nix build --print-build-logs 22 | - run: nix run . -- --help -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "shrinkwrap" 3 | version = "0.1.0" 4 | description = "A tool that embosses the needed dependencies on the top level executable." 5 | authors = ["Farid Zakaria "] 6 | license = "MIT" 7 | 8 | [tool.poetry.dependencies] 9 | python = "^3.9" 10 | sh = "^1.14.2" 11 | click = "^8.0.3" 12 | lief = "0.12.0" 13 | 14 | [tool.poetry.dev-dependencies] 15 | black = "^21.12b0" 16 | flake8 = "^4.0.1" 17 | mypy = "^0.930" 18 | isort = "^5.10.1" 19 | pytest = "^6.2.5" 20 | 21 | [tool.poetry.scripts] 22 | shrinkwrap = 'shrinkwrap.cli:shrinkwrap' 23 | 24 | [tool.isort] 25 | skip = [".git", "result"] 26 | profile = "black" 27 | 28 | [tool.pytest.ini_options] 29 | addopts = "" 30 | 31 | [build-system] 32 | requires = ["poetry-core>=1.0.0"] 33 | build-backend = "poetry.core.masonry.api" 34 | -------------------------------------------------------------------------------- /shrinkwrap/ldsoconf.py: -------------------------------------------------------------------------------- 1 | import functools 2 | from glob import glob 3 | from os.path import abspath, dirname, isabs, join 4 | from typing import Set 5 | 6 | 7 | # source: https://gist.github.com/stuaxo/79bcdcbaf9aa3b277207 8 | @functools.lru_cache() 9 | def parse(filename: str = "/etc/ld.so.conf") -> Set[str]: 10 | """Load all the paths from a given ldso config file""" 11 | paths = set() 12 | directory = dirname(abspath(filename)) 13 | with open(filename) as f: 14 | for line in (_line.rstrip() for _line in f.readlines()): 15 | if line.startswith("include "): 16 | wildcard = line.partition("include ")[-1:][0].rstrip() 17 | if not isabs(wildcard): 18 | wildcard = join(directory, wildcard) 19 | for filename in glob(wildcard): 20 | paths |= parse(filename) 21 | elif not line.startswith("#"): 22 | if line: 23 | paths.add(line) 24 | return paths 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Farid Zakaria 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /overlay.nix: -------------------------------------------------------------------------------- 1 | self: super: { 2 | 3 | # https://github.com/nix-community/poetry2nix/issues/218 4 | poetryOverrides = self: super: { 5 | typing-extensions = super.typing-extensions.overridePythonAttrs 6 | (old: { buildInputs = (old.buildInputs or [ ]) ++ [ self.flit-core ]; }); 7 | 8 | # This is an unreleased version of Lief that fixes a bug when generates GNU notes 9 | # https://github.com/lief-project/LIEF/commit/72ebe0d89e94c18d2b64da2cbbc7a0a0d53a5693 10 | lief = super.lief.overridePythonAttrs (old: { 11 | version = "0.12.72ebe0d"; 12 | src = super.pkgs.fetchFromGitHub { 13 | owner = "lief-project"; 14 | repo = "LIEF"; 15 | rev = "72ebe0d89e94c18d2b64da2cbbc7a0a0d53a5693"; 16 | sha256 = "039fwn6b92aq2vb8s44ld5bclz4gz2f9ki2kj7gy31x9lzjldnwk"; 17 | }; 18 | enableParallelBuilding = true; 19 | dontUseCmakeConfigure = true; 20 | nativeBuildInputs = [ self.pkgs.cmake ]; 21 | }); 22 | }; 23 | 24 | shrinkwrap = self.callPackage ./derivation.nix { }; 25 | 26 | shrinkwrap-env = self.poetry2nix.mkPoetryEnv { 27 | projectDir = ./.; 28 | overrides = [ self.poetry2nix.defaultPoetryOverrides self.poetryOverrides ]; 29 | editablePackageSources = { shrinkwrap = ./shrinkwrap; }; 30 | }; 31 | 32 | } 33 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "flake-utils": { 4 | "locked": { 5 | "lastModified": 1638122382, 6 | "narHash": "sha256-sQzZzAbvKEqN9s0bzWuYmRaA03v40gaJ4+iL1LXjaeI=", 7 | "owner": "numtide", 8 | "repo": "flake-utils", 9 | "rev": "74f7e4319258e287b0f9cb95426c9853b282730b", 10 | "type": "github" 11 | }, 12 | "original": { 13 | "owner": "numtide", 14 | "repo": "flake-utils", 15 | "type": "github" 16 | } 17 | }, 18 | "nixpkgs": { 19 | "locked": { 20 | "lastModified": 1640077788, 21 | "narHash": "sha256-YMSDk3hlucJTTARaHNOeQEF6zEW3A/x4sXgrz94VbS0=", 22 | "owner": "NixOS", 23 | "repo": "nixpkgs", 24 | "rev": "9ab7d12287ced0e1b4c03b61c781901f178d9d77", 25 | "type": "github" 26 | }, 27 | "original": { 28 | "owner": "NixOS", 29 | "ref": "nixos-21.11", 30 | "repo": "nixpkgs", 31 | "type": "github" 32 | } 33 | }, 34 | "poetry2nix": { 35 | "inputs": { 36 | "flake-utils": [ 37 | "flake-utils" 38 | ], 39 | "nixpkgs": [ 40 | "nixpkgs" 41 | ] 42 | }, 43 | "locked": { 44 | "lastModified": 1640144590, 45 | "narHash": "sha256-LCu2DJsJOPxMbQVMlfav+waBS/aeDzFvq/kJ2bnaR6k=", 46 | "owner": "nix-community", 47 | "repo": "poetry2nix", 48 | "rev": "f635203a774bf67226ed4f763af0efa3984e4136", 49 | "type": "github" 50 | }, 51 | "original": { 52 | "owner": "nix-community", 53 | "repo": "poetry2nix", 54 | "type": "github" 55 | } 56 | }, 57 | "root": { 58 | "inputs": { 59 | "flake-utils": "flake-utils", 60 | "nixpkgs": "nixpkgs", 61 | "poetry2nix": "poetry2nix" 62 | } 63 | } 64 | }, 65 | "root": "root", 66 | "version": 7 67 | } 68 | -------------------------------------------------------------------------------- /shrinkwrap/cli.py: -------------------------------------------------------------------------------- 1 | import os 2 | from shutil import copystat 3 | from typing import Optional 4 | 5 | import click 6 | import lief # type: ignore 7 | 8 | from shrinkwrap.elf import LinkStrategy 9 | 10 | 11 | @click.command() 12 | @click.argument("file", type=click.Path(exists=True)) 13 | @click.option("-o", "--output", type=click.Path(), required=False) 14 | @click.option( 15 | "-l", 16 | "--link-strategy", 17 | default="native", 18 | show_default=True, 19 | type=click.Choice(["native", "virtual"], case_sensitive=True), 20 | ) 21 | def shrinkwrap(file: str, output: Optional[str], link_strategy: str): 22 | """Freeze the dependencies into the top level shared object file.""" 23 | if output is None: 24 | output = os.path.basename(file) + "_stamped" 25 | 26 | if not lief.is_elf(file): 27 | click.echo(f"{file} is not elf format") 28 | exit(1) 29 | 30 | binary: lief.Binary = lief.parse(file) 31 | if not binary.has_interpreter: 32 | click.echo("no interpreter set on the binary") 33 | exit(1) 34 | 35 | strategy = LinkStrategy.select_by_name(link_strategy) 36 | resolution = strategy.explore(binary, file) 37 | needed = list(binary.libraries) 38 | for name in needed: 39 | binary.remove_library(name) 40 | 41 | for soname, lib in reversed(resolution.items()): 42 | binary.add_library(lib) 43 | 44 | # we need to update the VERNEED entries now to match 45 | verneeded = binary.symbols_version_requirement 46 | for verneed in verneeded: 47 | if verneed.name in resolution: 48 | # we want to map the possible shortname soname 49 | # to the absolute one we generate 50 | verneed.name = resolution.get(verneed.name) 51 | 52 | # dump the new binary file 53 | binary.write(output) 54 | 55 | # copy the file metadata 56 | copystat(file, output) 57 | 58 | 59 | if __name__ == "__main__": 60 | shrinkwrap() 61 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "Shrinkwrap and freeze the dynamic dependencies of binaries."; 3 | 4 | inputs = { 5 | nixpkgs.url = "github:NixOS/nixpkgs/nixos-21.11"; 6 | flake-utils.url = "github:numtide/flake-utils"; 7 | poetry2nix = { 8 | url = "github:nix-community/poetry2nix"; 9 | inputs = { 10 | flake-utils.follows = "flake-utils"; 11 | nixpkgs.follows = "nixpkgs"; 12 | }; 13 | }; 14 | }; 15 | 16 | outputs = { self, nixpkgs, flake-utils, poetry2nix }: 17 | flake-utils.lib.eachDefaultSystem (system: 18 | let 19 | pkgs = import nixpkgs { 20 | inherit system; 21 | overlays = [ poetry2nix.overlay (import ./overlay.nix) ]; 22 | }; 23 | runCodeAnalysis = name: command: 24 | pkgs.runCommand "shrinkwrap-${name}-check" { } '' 25 | cd ${self} 26 | ${command} 27 | mkdir $out 28 | ''; 29 | in 30 | { 31 | packages = { 32 | shrinkwrap = pkgs.shrinkwrap; 33 | }; 34 | 35 | legacyPackages = { 36 | experiments = { 37 | emacs = pkgs.dockerTools.buildImage { 38 | name = "shrinkwrap-emacs-experiment"; 39 | contents = [ 40 | pkgs.strace 41 | pkgs.emacs 42 | pkgs.shrinkwrap 43 | pkgs.bashInteractive 44 | pkgs.binutils 45 | pkgs.patchelf 46 | ]; 47 | runAsRoot = '' 48 | # this directory does not exist and is needed by shrinkwrap 49 | mkdir /dev/fd 50 | shrinkwrap ${pkgs.emacs}/bin/.emacs-27.2-wrapped -o /bin/emacs-stamped 51 | ''; 52 | }; 53 | }; 54 | }; 55 | 56 | checks = { 57 | pytest-check = runCodeAnalysis "pytest" '' 58 | ${pkgs.shrinkwrap-env}/bin/pytest -p no:cacheprovider . 59 | ''; 60 | black-check = runCodeAnalysis "black" '' 61 | ${pkgs.shrinkwrap-env}/bin/black --check . 62 | ''; 63 | mypy-check = runCodeAnalysis "mypy" '' 64 | ${pkgs.shrinkwrap-env}/bin/mypy . 65 | ''; 66 | isort-check = runCodeAnalysis "isort" '' 67 | ${pkgs.shrinkwrap-env}/bin/isort -c . 68 | ''; 69 | flake8-check = runCodeAnalysis "flake8" '' 70 | ${pkgs.shrinkwrap-env}/bin/flake8 . 71 | ''; 72 | nixpkgs-fmt-check = runCodeAnalysis "nixpkgs-fmt" '' 73 | ${pkgs.nixpkgs-fmt}/bin/nixpkgs-fmt --check . 74 | ''; 75 | }; 76 | 77 | defaultPackage = pkgs.shrinkwrap; 78 | 79 | devShell = pkgs.shrinkwrap-env.env.overrideAttrs (old: { 80 | nativeBuildInputs = with pkgs; 81 | old.nativeBuildInputs ++ [ poetry nixpkgs-fmt nix-linter ]; 82 | }); 83 | 84 | }); 85 | } 86 | -------------------------------------------------------------------------------- /shrinkwrap/elf.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import os 4 | import re 5 | from abc import ABC, abstractmethod 6 | from collections import OrderedDict 7 | from typing import Dict, Iterable, Optional 8 | 9 | import lief # type: ignore 10 | from sh import Command # type: ignore 11 | 12 | from shrinkwrap import ldsoconf 13 | 14 | 15 | class LinkStrategy(ABC): 16 | @staticmethod 17 | def select_by_name(name: str) -> LinkStrategy: 18 | if name == "native": 19 | return NativeLinkStrategy() 20 | elif name == "virtual": 21 | return VirtualLinkStrategy() 22 | else: 23 | raise Exception(f"Unknown strategy: {name}") 24 | 25 | @abstractmethod 26 | def explore(self, binary: lief.Binary, filename: str) -> Dict[str, str]: 27 | """ 28 | Determine the linking for all needed objects 29 | """ 30 | pass 31 | 32 | 33 | class NativeLinkStrategy(LinkStrategy): 34 | """Uses the native interpreter in the binary to determine the linking""" 35 | 36 | def explore(self, binary: lief.Binary, filename: str) -> Dict[str, str]: 37 | interpreter = Command(binary.interpreter) 38 | resolution = interpreter("--list", filename) 39 | result = OrderedDict() 40 | # TODO: Figure out why `--list` and `ldd` produce different outcomes 41 | # specifically for the interpreter. 42 | # https://gist.github.com/fzakaria/3dc42a039401598d8e0fdbc57f5e7eae 43 | for line in resolution.splitlines(): 44 | m = re.match(r"\s*([^ ]+) => ([^ ]+)", line) 45 | if not m: 46 | continue 47 | soname, lib = m.group(1), m.group(2) 48 | result[soname] = lib 49 | return result 50 | 51 | 52 | class VirtualLinkStrategy(LinkStrategy): 53 | 54 | # TODO: Need to figure out a good way to determine the NEEDED of glibc 55 | # I think it's resolving based on a shared object cache from the .INTERP 56 | # section but that remains to be validated. 57 | SKIP = ["ld-linux.so.2", "ld-linux-x86-64.so.2"] 58 | 59 | @staticmethod 60 | def find( 61 | paths: Iterable[str], 62 | soname: str, 63 | identity_class: lief.ELF.ELF_CLASS, 64 | machine_type: lief.ELF.ARCH, 65 | ) -> Optional[str]: 66 | """Given a list of paths, try and find it. It does not search recursively""" 67 | for path in paths: 68 | full_path = os.path.join(path, soname) 69 | if os.path.exists(full_path): 70 | if not lief.is_elf(full_path): 71 | continue 72 | binary = lief.parse(full_path) 73 | if ( 74 | binary.header.identity_class != identity_class 75 | or binary.header.machine_type != machine_type 76 | ): 77 | continue 78 | return full_path 79 | return None 80 | 81 | @staticmethod 82 | def has_nodeflib(binary: lief.Binary) -> bool: 83 | if not binary.has(lief.ELF.DYNAMIC_TAGS.FLAGS_1): 84 | return False 85 | for flag in binary[lief.ELF.DYNAMIC_TAGS.FLAGS_1].flags: 86 | if flag == lief.ELF.DYNAMIC_FLAGS_1.NODEFLIB: 87 | return True 88 | return False 89 | 90 | def explore(self, binary: lief.Binary, filename: str) -> Dict[str, str]: 91 | """ 92 | Determine the linking for all needed objects 93 | """ 94 | 95 | result = OrderedDict() 96 | queue = [binary] 97 | rpaths = [] 98 | ld_library_path = os.environ.get("LD_LIBRARY_PATH", "").split(":") 99 | default_paths = ldsoconf.parse() 100 | seen = set() 101 | 102 | # The following is a rough translation of the search as described in 103 | # https://man7.org/linux/man-pages/man8/ld.so.8.html 104 | # 1. IF RUNPATH is not present, and RPATH is present use RPATH. 105 | # Note: RPATH is cumaltive as it traverses the children 106 | # 2. Use the environment variable LD_LIBRARY_PATH 107 | # 3. Use RUNPATH to locate only the current shared objects dependencies 108 | # 4. Default libraries, unless ELF file has 'nodeflibs' set 109 | while len(queue) > 0: 110 | current = queue.pop() 111 | 112 | if current.has(lief.ELF.DYNAMIC_TAGS.RPATH): 113 | rpaths += current.get(lief.ELF.DYNAMIC_TAGS.RPATH).paths 114 | 115 | runpaths = [] 116 | if current.has(lief.ELF.DYNAMIC_TAGS.RUNPATH): 117 | runpaths += current.get(lief.ELF.DYNAMIC_TAGS.RUNPATH).paths 118 | 119 | needed = current.libraries 120 | 121 | # any binaries found need to make sure we match 122 | # the identity_class and machine_type 123 | identity_class = current.header.identity_class 124 | machine_type = current.header.machine_type 125 | 126 | for soname in needed: 127 | 128 | if soname in VirtualLinkStrategy.SKIP: 129 | continue 130 | 131 | if os.path.basename(soname) in seen: 132 | continue 133 | 134 | path = None 135 | # IF RUNPATH is not present, and RPATH is present use RPATH. 136 | if not path and len(runpaths) == 0 and len(rpaths) > 0: 137 | path = VirtualLinkStrategy.find( 138 | rpaths, soname, identity_class, machine_type 139 | ) 140 | # Use the environment variable LD_LIBRARY_PATH 141 | if not path and len(ld_library_path) > 0: 142 | path = VirtualLinkStrategy.find( 143 | ld_library_path, soname, identity_class, machine_type 144 | ) 145 | if path: 146 | result[soname] = path 147 | 148 | # Use RUNPATH to locate only the current shared objects dependencies 149 | if not path and len(runpaths) > 0: 150 | path = VirtualLinkStrategy.find( 151 | runpaths, soname, identity_class, machine_type 152 | ) 153 | 154 | if not path and not VirtualLinkStrategy.has_nodeflib(current): 155 | path = VirtualLinkStrategy.find( 156 | default_paths, soname, identity_class, machine_type 157 | ) 158 | 159 | if not path: 160 | raise Exception(f"Could not find {soname}") 161 | 162 | # lets add the basename of the soname to a cache 163 | # so that any object that requires the same soname is skipped 164 | # this works since this is the same behavior as in glibc 165 | seen.add(os.path.basename(soname)) 166 | 167 | result[soname] = path 168 | queue.append(lief.parse(path)) 169 | 170 | return result 171 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Shrinkwrap 2 | 3 | ![main](https://github.com/fzakaria/shrinkwrap/actions/workflows/main.yml/badge.svg) 4 | [![built with nix](https://builtwithnix.org/badge.svg)](https://builtwithnix.org) 5 | 6 | > A tool that embosses the needed dependencies on the top level executable 7 | 8 | # Introduction 9 | 10 | It can be useful to _freeze_ all the dynamic shared objects needed by an application. 11 | 12 | _shrinkwrap_ is a tool which will discover all transitive dynamic shared objects, and lift them up to the executable referenced by absolute path. 13 | 14 | Here is an example where we will apply this to _ruby_. 15 | 16 | Lets take a look at all the _dynamic shared objects_ needed by the Ruby interpreter. 17 | 18 | ```console 19 | ❯ ldd $(which ruby) 20 | linux-vdso.so.1 (0x00007ffeed386000) 21 | /lib/x86_64-linux-gnu/libnss_cache.so.2 (0x00007f638ddf8000) 22 | libruby-2.7.so.2.7 => /lib/x86_64-linux-gnu/libruby-2.7.so.2.7 (0x00007f638da79000) 23 | libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f638d8b4000) 24 | libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007f638d893000) 25 | librt.so.1 => /lib/x86_64-linux-gnu/librt.so.1 (0x00007f638d888000) 26 | libgmp.so.10 => /lib/x86_64-linux-gnu/libgmp.so.10 (0x00007f638d807000) 27 | libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007f638d7ff000) 28 | libcrypt.so.1 => /lib/x86_64-linux-gnu/libcrypt.so.1 (0x00007f638d7c4000) 29 | libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007f638d67f000) 30 | /lib64/ld-linux-x86-64.so.2 (0x00007f638de06000) 31 | ``` 32 | 33 | We can see also that the _ruby_ application only lists a few needed shared objects itself. 34 | 35 | ```console 36 | ❯ patchelf --print-needed $(which ruby) 37 | libruby-2.7.so.2.7 38 | libc.so.6 39 | ``` 40 | 41 | Let's now apply _shrinkwrap_ and see the results. 42 | 43 | ```console 44 | ❯ nix run github:fzakaria/shrinkwrap $(which ruby) 45 | ``` 46 | 47 | It automatically creates a `_stamped` copy of the filename if none provided and sets all the _NEEDED_ sections. 48 | 49 | ```console 50 | ❯ patchelf --print-needed ruby_stamped 51 | /lib/x86_64-linux-gnu/libm.so.6 52 | /lib/x86_64-linux-gnu/libcrypt.so.1 53 | /lib/x86_64-linux-gnu/libdl.so.2 54 | /lib/x86_64-linux-gnu/libgmp.so.10 55 | /lib/x86_64-linux-gnu/librt.so.1 56 | /lib/x86_64-linux-gnu/libpthread.so.0 57 | /lib/x86_64-linux-gnu/libruby-2.7.so.2.7 58 | /lib/x86_64-linux-gnu/libc.so.6 59 | 60 | ❯ ldd ruby_stamped 61 | linux-vdso.so.1 (0x00007ffe641f3000) 62 | /lib/x86_64-linux-gnu/libnss_cache.so.2 (0x00007f9cd4320000) 63 | /lib/x86_64-linux-gnu/libm.so.6 (0x00007f9cd41db000) 64 | /lib/x86_64-linux-gnu/libcrypt.so.1 (0x00007f9cd41a0000) 65 | /lib/x86_64-linux-gnu/libdl.so.2 (0x00007f9cd419a000) 66 | /lib/x86_64-linux-gnu/libgmp.so.10 (0x00007f9cd4119000) 67 | /lib/x86_64-linux-gnu/librt.so.1 (0x00007f9cd410e000) 68 | /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007f9cd40eb000) 69 | /lib/x86_64-linux-gnu/libruby-2.7.so.2.7 (0x00007f9cd3d8c000) 70 | /lib/x86_64-linux-gnu/libc.so.6 (0x00007f9cd3bc7000) 71 | /lib64/ld-linux-x86-64.so.2 (0x00007f9cd4336000) 72 | ``` 73 | 74 | ## Motivation 75 | 76 | Certain _store_ based build tools such as [Guix](https://guix.gnu.org/), [Nix](https://nixos.org) or [Spack](https://spack.io/) make heavy use of _RUNPATH_ to help create reproducible and hermetic binaries. 77 | 78 | One problem with the heavy use of _RUNPATH_, is that the search space could effect startup as it's `O(n)` on the number of entries (potentially worse if using _RPATH_). This can alo be expensive in _stat syscalls_, that has been well documented by in [this blog post](https://guix.gnu.org/blog/2021/taming-the-stat-storm-with-a-loader-cache/). 79 | 80 | Secondly, shared dynamic objects may be found due to the fact that they are cached during the linking stage. Meaning, if another shared object requires the same dependency but failed to specify where to find it, it may still properly resolved if discovered earlier in the linking process. This is extremely error prone and changing any of the executable's dependencies can change the link order and potentially cause the binary to no longer work. 81 | 82 | Lifting up the needed shared objects to the top executable makes the dependency discovery _simple_, _quick_ and _hermetic_ since it can no longer change based on the order of visited dependencies. 83 | 84 | ## Pitfalls 85 | 86 | At the moment this only works with _glibc_ and not other _Standard C Libraries_ such as _musl_. The reason is that other linkers seem to resolve duplicate shared object files differently when they appear in the traversal. Consider the following example: 87 | 88 | ``` 89 | +------------+ 90 | | | 91 | | Executable | 92 | | | 93 | +-------+------------+----+ 94 | | | 95 | | | 96 | +-----v-----+ +------v----+ 97 | | | | | 98 | | libbar.so | | libfoo.so | 99 | | | | | 100 | +-----+-----+ +-----------+ 101 | | /some-fixed-path/libfoo.so 102 | | 103 | +-----v------+ 104 | | | 105 | | libfoo.so | 106 | | | 107 | +------------+ 108 | ``` 109 | 110 | In _glibc_ the cache is keyed by the _soname_ value on the shared object. That allows the first found _libfoo.so_ at _/some-fixed-path/libfoo.so_ to be used for the one which _libbar.so_ depends on. 111 | 112 | Unfortunately, _musl_ does not support this functionality and ongoing discussions of inclusing it can be followed on the [mailing list](https://www.openwall.com/lists/musl/2021/12/21/1). 113 | 114 | ## Development 115 | 116 | You must have [Nix](https://nixos.org) installed for development. 117 | 118 | This package uses [poetry2nix](https://github.com/nix-community/poetry2nix) to easily setup a development environment. 119 | 120 | ```console 121 | > nix develop 122 | ``` 123 | 124 | A helping `Makefile` is provided to run all the _linters_ and _formatters_. 125 | 126 | ```console 127 | > make lint 128 | ``` 129 | 130 | Note: I publish artifacts to [cachix](https://cachix.org/) that you can use to develop faster. 131 | ```console 132 | > cachix use fzakaria 133 | ``` 134 | 135 | ## Experiments 136 | 137 | Included in the flake are different experiments for evaluating Shrinkwrap. 138 | In most cases they provide a Docker image (tar.gz) which can be loaded. 139 | 140 | ### emacs 141 | 142 | Creates a stamped version of the popular emacs editor similarly to the Guix experiment outlined in the [blog post](https://guix.gnu.org/blog/2021/taming-the-stat-storm-with-a-loader-cache/). 143 | 144 | You can build the Docker image and inside will be `emacs-wrapped` as well as `emacs` and `strace` to recreate the experiment. 145 | ```console 146 | > nix build .#experiments.emacs 147 | > docker load < result 148 | 643ace721190: Loading layer [==================================================>] 786.9MB/786.9MB 149 | Loaded image: shrinkwrap-emacs-experiment:7jjlknqq660x1crrw7gm4m2qzalp71qj 150 | > docker run -it emacs-experiment:7jjlknqq660x1crrw7gm4m2qzalp71qj /bin/bash 151 | > patchelf --print-needed /bin/emacs-stamped 152 | /nix/store/m756011mkf1i0ki78i8y6ac3gf8qphvi-gcc-10.3.0-lib/lib/libstdc++.so.6 153 | /nix/store/xif6gg595hgmqawrcarapa8j2r7fbz9w-icu4c-70.1/lib/libicudata.so.70 154 | /nix/store/i6cmh2d4hbyp00rnh5rpf48pc7xfzx6j-libgpg-error-1.42/lib/libgpg-error.so.0 155 | /nix/store/q39ykk5fnhlbnl119iqjbgaw44kd65fy-util-linux-2.37.2-lib/lib/libblkid.so.1 156 | /nix/store/b1k5z0fdj0pnfz89k440al7ww4a263bf-libglvnd-1.3.4/lib/libGLX.so.0 157 | 158 | ``` 159 | 160 | If you'd like you can pull the image directly from DockerHub via [fmzakari/shrinkwrap-emacs-experiment:7jjlknqq660x1crrw7gm4m2qzalp71qj](https://hub.docker.com/layers/shrinkwrap-emacs-experiment/fmzakari/shrinkwrap-emacs-experiment/7jjlknqq660x1crrw7gm4m2qzalp71qj/images/sha256-4633059bdf6c7ddbe23a4c6da11eba7ff58029eb870af01c98f10ada03324ee0?context=explore). 161 | 162 | ```console 163 | > docker pull fmzakari/shrinkwrap-emacs-experiment:7jjlknqq660x1crrw7gm4m2qzalp71qj 164 | > docker run -it fmzakari/shrinkwrap-emacs-experiment:7jjlknqq660x1crrw7gm4m2qzalp71qj /bin/bash 165 | > patchelf --print-needed /bin/emacs-stamped 166 | /nix/store/m756011mkf1i0ki78i8y6ac3gf8qphvi-gcc-10.3.0-lib/lib/libstdc++.so.6 167 | /nix/store/xif6gg595hgmqawrcarapa8j2r7fbz9w-icu4c-70.1/lib/libicudata.so.70 168 | /nix/store/i6cmh2d4hbyp00rnh5rpf48pc7xfzx6j-libgpg-error-1.42/lib/libgpg-error.so.0 169 | /nix/store/q39ykk5fnhlbnl119iqjbgaw44kd65fy-util-linux-2.37.2-lib/lib/libblkid.so.1 170 | /nix/store/b1k5z0fdj0pnfz89k440al7ww4a263bf-libglvnd-1.3.4/lib/libGLX.so.0 171 | ``` 172 | ## Contributions 173 | 174 | Thanks to [@trws](https://github.com/trws) for the inspiration and original version of this Python script. -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | [[package]] 2 | name = "atomicwrites" 3 | version = "1.4.0" 4 | description = "Atomic file writes." 5 | category = "dev" 6 | optional = false 7 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 8 | 9 | [[package]] 10 | name = "attrs" 11 | version = "21.2.0" 12 | description = "Classes Without Boilerplate" 13 | category = "dev" 14 | optional = false 15 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 16 | 17 | [package.extras] 18 | dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit"] 19 | docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"] 20 | tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface"] 21 | tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins"] 22 | 23 | [[package]] 24 | name = "black" 25 | version = "21.12b0" 26 | description = "The uncompromising code formatter." 27 | category = "dev" 28 | optional = false 29 | python-versions = ">=3.6.2" 30 | 31 | [package.dependencies] 32 | click = ">=7.1.2" 33 | mypy-extensions = ">=0.4.3" 34 | pathspec = ">=0.9.0,<1" 35 | platformdirs = ">=2" 36 | tomli = ">=0.2.6,<2.0.0" 37 | typing-extensions = [ 38 | {version = ">=3.10.0.0", markers = "python_version < \"3.10\""}, 39 | {version = "!=3.10.0.1", markers = "python_version >= \"3.10\""}, 40 | ] 41 | 42 | [package.extras] 43 | colorama = ["colorama (>=0.4.3)"] 44 | d = ["aiohttp (>=3.7.4)"] 45 | jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] 46 | python2 = ["typed-ast (>=1.4.3)"] 47 | uvloop = ["uvloop (>=0.15.2)"] 48 | 49 | [[package]] 50 | name = "click" 51 | version = "8.0.3" 52 | description = "Composable command line interface toolkit" 53 | category = "main" 54 | optional = false 55 | python-versions = ">=3.6" 56 | 57 | [package.dependencies] 58 | colorama = {version = "*", markers = "platform_system == \"Windows\""} 59 | 60 | [[package]] 61 | name = "colorama" 62 | version = "0.4.4" 63 | description = "Cross-platform colored terminal text." 64 | category = "main" 65 | optional = false 66 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 67 | 68 | [[package]] 69 | name = "flake8" 70 | version = "4.0.1" 71 | description = "the modular source code checker: pep8 pyflakes and co" 72 | category = "dev" 73 | optional = false 74 | python-versions = ">=3.6" 75 | 76 | [package.dependencies] 77 | mccabe = ">=0.6.0,<0.7.0" 78 | pycodestyle = ">=2.8.0,<2.9.0" 79 | pyflakes = ">=2.4.0,<2.5.0" 80 | 81 | [[package]] 82 | name = "iniconfig" 83 | version = "1.1.1" 84 | description = "iniconfig: brain-dead simple config-ini parsing" 85 | category = "dev" 86 | optional = false 87 | python-versions = "*" 88 | 89 | [[package]] 90 | name = "isort" 91 | version = "5.10.1" 92 | description = "A Python utility / library to sort Python imports." 93 | category = "dev" 94 | optional = false 95 | python-versions = ">=3.6.1,<4.0" 96 | 97 | [package.extras] 98 | pipfile_deprecated_finder = ["pipreqs", "requirementslib"] 99 | requirements_deprecated_finder = ["pipreqs", "pip-api"] 100 | colors = ["colorama (>=0.4.3,<0.5.0)"] 101 | plugins = ["setuptools"] 102 | 103 | [[package]] 104 | name = "lief" 105 | version = "0.11.5" 106 | description = "Library to instrument executable formats" 107 | category = "main" 108 | optional = false 109 | python-versions = ">=3.6" 110 | 111 | [[package]] 112 | name = "mccabe" 113 | version = "0.6.1" 114 | description = "McCabe checker, plugin for flake8" 115 | category = "dev" 116 | optional = false 117 | python-versions = "*" 118 | 119 | [[package]] 120 | name = "mypy" 121 | version = "0.930" 122 | description = "Optional static typing for Python" 123 | category = "dev" 124 | optional = false 125 | python-versions = ">=3.6" 126 | 127 | [package.dependencies] 128 | mypy-extensions = ">=0.4.3" 129 | tomli = ">=1.1.0" 130 | typing-extensions = ">=3.10" 131 | 132 | [package.extras] 133 | dmypy = ["psutil (>=4.0)"] 134 | python2 = ["typed-ast (>=1.4.0,<2)"] 135 | 136 | [[package]] 137 | name = "mypy-extensions" 138 | version = "0.4.3" 139 | description = "Experimental type system extensions for programs checked with the mypy typechecker." 140 | category = "dev" 141 | optional = false 142 | python-versions = "*" 143 | 144 | [[package]] 145 | name = "packaging" 146 | version = "21.3" 147 | description = "Core utilities for Python packages" 148 | category = "dev" 149 | optional = false 150 | python-versions = ">=3.6" 151 | 152 | [package.dependencies] 153 | pyparsing = ">=2.0.2,<3.0.5 || >3.0.5" 154 | 155 | [[package]] 156 | name = "pathspec" 157 | version = "0.9.0" 158 | description = "Utility library for gitignore style pattern matching of file paths." 159 | category = "dev" 160 | optional = false 161 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" 162 | 163 | [[package]] 164 | name = "platformdirs" 165 | version = "2.4.0" 166 | description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." 167 | category = "dev" 168 | optional = false 169 | python-versions = ">=3.6" 170 | 171 | [package.extras] 172 | docs = ["Sphinx (>=4)", "furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)"] 173 | test = ["appdirs (==1.4.4)", "pytest (>=6)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)"] 174 | 175 | [[package]] 176 | name = "pluggy" 177 | version = "1.0.0" 178 | description = "plugin and hook calling mechanisms for python" 179 | category = "dev" 180 | optional = false 181 | python-versions = ">=3.6" 182 | 183 | [package.extras] 184 | dev = ["pre-commit", "tox"] 185 | testing = ["pytest", "pytest-benchmark"] 186 | 187 | [[package]] 188 | name = "py" 189 | version = "1.11.0" 190 | description = "library with cross-python path, ini-parsing, io, code, log facilities" 191 | category = "dev" 192 | optional = false 193 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 194 | 195 | [[package]] 196 | name = "pycodestyle" 197 | version = "2.8.0" 198 | description = "Python style guide checker" 199 | category = "dev" 200 | optional = false 201 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 202 | 203 | [[package]] 204 | name = "pyflakes" 205 | version = "2.4.0" 206 | description = "passive checker of Python programs" 207 | category = "dev" 208 | optional = false 209 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 210 | 211 | [[package]] 212 | name = "pyparsing" 213 | version = "3.0.6" 214 | description = "Python parsing module" 215 | category = "dev" 216 | optional = false 217 | python-versions = ">=3.6" 218 | 219 | [package.extras] 220 | diagrams = ["jinja2", "railroad-diagrams"] 221 | 222 | [[package]] 223 | name = "pytest" 224 | version = "6.2.5" 225 | description = "pytest: simple powerful testing with Python" 226 | category = "dev" 227 | optional = false 228 | python-versions = ">=3.6" 229 | 230 | [package.dependencies] 231 | atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} 232 | attrs = ">=19.2.0" 233 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 234 | iniconfig = "*" 235 | packaging = "*" 236 | pluggy = ">=0.12,<2.0" 237 | py = ">=1.8.2" 238 | toml = "*" 239 | 240 | [package.extras] 241 | testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] 242 | 243 | [[package]] 244 | name = "sh" 245 | version = "1.14.2" 246 | description = "Python subprocess replacement" 247 | category = "main" 248 | optional = false 249 | python-versions = "*" 250 | 251 | [[package]] 252 | name = "toml" 253 | version = "0.10.2" 254 | description = "Python Library for Tom's Obvious, Minimal Language" 255 | category = "dev" 256 | optional = false 257 | python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" 258 | 259 | [[package]] 260 | name = "tomli" 261 | version = "1.2.3" 262 | description = "A lil' TOML parser" 263 | category = "dev" 264 | optional = false 265 | python-versions = ">=3.6" 266 | 267 | [[package]] 268 | name = "typing-extensions" 269 | version = "4.0.1" 270 | description = "Backported and Experimental Type Hints for Python 3.6+" 271 | category = "dev" 272 | optional = false 273 | python-versions = ">=3.6" 274 | 275 | [metadata] 276 | lock-version = "1.1" 277 | python-versions = "^3.9" 278 | content-hash = "1a49e5400de05a46967ee843695ee8f308e0d1bbb108a6151a6dc25ce3734bd7" 279 | 280 | [metadata.files] 281 | atomicwrites = [ 282 | {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, 283 | {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, 284 | ] 285 | attrs = [ 286 | {file = "attrs-21.2.0-py2.py3-none-any.whl", hash = "sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1"}, 287 | {file = "attrs-21.2.0.tar.gz", hash = "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb"}, 288 | ] 289 | black = [ 290 | {file = "black-21.12b0-py3-none-any.whl", hash = "sha256:a615e69ae185e08fdd73e4715e260e2479c861b5740057fde6e8b4e3b7dd589f"}, 291 | {file = "black-21.12b0.tar.gz", hash = "sha256:77b80f693a569e2e527958459634f18df9b0ba2625ba4e0c2d5da5be42e6f2b3"}, 292 | ] 293 | click = [ 294 | {file = "click-8.0.3-py3-none-any.whl", hash = "sha256:353f466495adaeb40b6b5f592f9f91cb22372351c84caeb068132442a4518ef3"}, 295 | {file = "click-8.0.3.tar.gz", hash = "sha256:410e932b050f5eed773c4cda94de75971c89cdb3155a72a0831139a79e5ecb5b"}, 296 | ] 297 | colorama = [ 298 | {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, 299 | {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, 300 | ] 301 | flake8 = [ 302 | {file = "flake8-4.0.1-py2.py3-none-any.whl", hash = "sha256:479b1304f72536a55948cb40a32dce8bb0ffe3501e26eaf292c7e60eb5e0428d"}, 303 | {file = "flake8-4.0.1.tar.gz", hash = "sha256:806e034dda44114815e23c16ef92f95c91e4c71100ff52813adf7132a6ad870d"}, 304 | ] 305 | iniconfig = [ 306 | {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, 307 | {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, 308 | ] 309 | isort = [ 310 | {file = "isort-5.10.1-py3-none-any.whl", hash = "sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7"}, 311 | {file = "isort-5.10.1.tar.gz", hash = "sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951"}, 312 | ] 313 | lief = [ 314 | {file = "lief-0.11.5-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:1cca100e77382f4137a3b1283769efa0e68a965fa4f3e21e64e3f67b6e22fdc8"}, 315 | {file = "lief-0.11.5-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:621ad19f77884a008d61e05b92aed8309a8460e93916f4722439beaa529ca37d"}, 316 | {file = "lief-0.11.5-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c672dcd78dbbe2c0746721cdb1593b237a8b983d039e73713b055449e4a58207"}, 317 | {file = "lief-0.11.5-cp36-cp36m-win32.whl", hash = "sha256:5a0da170943aaf7019b27b9a7199b860298426c0455f88add392f472605c39ee"}, 318 | {file = "lief-0.11.5-cp36-cp36m-win_amd64.whl", hash = "sha256:5f5fb42461b5d5d5b2ccf7fe17e8f26bd632afcbaedf29a9d30819eeea5dab29"}, 319 | {file = "lief-0.11.5-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:710112ebc642bf5287a7b25c54c8a4e1079cbb403d4e844a364e1c3cbed52486"}, 320 | {file = "lief-0.11.5-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:bfc0246af63361e22a952f8babd542477d64288d993c5a053a72f9e3f59da795"}, 321 | {file = "lief-0.11.5-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:8b219ce4a41b0734fe9a7fbfde7d23a92bc005c8684882662808fc438568c1b5"}, 322 | {file = "lief-0.11.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f510836d19cee407015ee565ea566e444471f0ecb3028a5c5e2219a7583f3c4"}, 323 | {file = "lief-0.11.5-cp37-cp37m-win32.whl", hash = "sha256:9c6cc9da3e3a56ad29fc4e77e7109e960bd0cae3e3ba5307e3ae5c65d85fbdc4"}, 324 | {file = "lief-0.11.5-cp37-cp37m-win_amd64.whl", hash = "sha256:a1f7792f1d811a898d3d676c32731d6b055573a2c3e67988ab1b32917db3de96"}, 325 | {file = "lief-0.11.5-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:fd41077526e30bfcafa3d03bff8466a4a9ae4bbe21cadd6a09168a62ce18710c"}, 326 | {file = "lief-0.11.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5122e4e70fecc32e7fdf2e9cd9b580ddd63fb4509eae373be78b3c11d67175b8"}, 327 | {file = "lief-0.11.5-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:e6d9621c1db852ca4de37efe98151838edf0a976fe03cace471b3a761861f95e"}, 328 | {file = "lief-0.11.5-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:17314177c0124ccd450554bbcb203b8cd2660c94e36bdc05a6eba04bb0af3954"}, 329 | {file = "lief-0.11.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b275a542b5ef173ec9602d2f511a895c4228db63bbbc58699859da4afe8bfd58"}, 330 | {file = "lief-0.11.5-cp38-cp38-win32.whl", hash = "sha256:208294f208354f57ded772efc4c3b2ea61fae35325a048d38c21571cb35e4bfc"}, 331 | {file = "lief-0.11.5-cp38-cp38-win_amd64.whl", hash = "sha256:f4e8a878615a46ef4ae016261a59152b8c019a35adb865e26a37c8ef25200d7e"}, 332 | {file = "lief-0.11.5-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:544b0f8a587bc5f6fd39cf47d9785af2714f982682efcd1dd3291604e7cb6351"}, 333 | {file = "lief-0.11.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e743345290649f54efcf2c1ea530f3520a7b22583fb8b0772df48b1901ecb1ea"}, 334 | {file = "lief-0.11.5-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:eb8c2ae617ff54c4ea73dbd055544681b3cfeafbdbf0fe4535fac494515ab65b"}, 335 | {file = "lief-0.11.5-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:a4bb649a2f5404b8e2e4b8beb3772466430e7382fc5f7f014f3f778137133987"}, 336 | {file = "lief-0.11.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:44bd7804a39837ff46cd543154f6e4a28e2d4fafa312752ca6deea1c849995ce"}, 337 | {file = "lief-0.11.5-cp39-cp39-win32.whl", hash = "sha256:8fd1ecdb3001e8e19df7278b77df5d6394ad6071354e177d11ad08b0a727d390"}, 338 | {file = "lief-0.11.5-cp39-cp39-win_amd64.whl", hash = "sha256:c773eaee900f398cc98a9c8501d9ab7465af9729979841bb78f4aaa8b821fd9a"}, 339 | {file = "lief-0.11.5.zip", hash = "sha256:932ba495388fb52b4ba056a0b00abe0bda3567ad3ebc6d726be1e87b8be08b3f"}, 340 | ] 341 | mccabe = [ 342 | {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, 343 | {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, 344 | ] 345 | mypy = [ 346 | {file = "mypy-0.930-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:221cc94dc6a801ccc2be7c0c9fd791c5e08d1fa2c5e1c12dec4eab15b2469871"}, 347 | {file = "mypy-0.930-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:db3a87376a1380f396d465bed462e76ea89f838f4c5e967d68ff6ee34b785c31"}, 348 | {file = "mypy-0.930-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1d2296f35aae9802eeb1327058b550371ee382d71374b3e7d2804035ef0b830b"}, 349 | {file = "mypy-0.930-cp310-cp310-win_amd64.whl", hash = "sha256:959319b9a3cafc33a8185f440a433ba520239c72e733bf91f9efd67b0a8e9b30"}, 350 | {file = "mypy-0.930-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:45a4dc21c789cfd09b8ccafe114d6de66f0b341ad761338de717192f19397a8c"}, 351 | {file = "mypy-0.930-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1e689e92cdebd87607a041585f1dc7339aa2e8a9f9bad9ba7e6ece619431b20c"}, 352 | {file = "mypy-0.930-cp36-cp36m-win_amd64.whl", hash = "sha256:ed4e0ea066bb12f56b2812a15ff223c57c0a44eca817ceb96b214bb055c7051f"}, 353 | {file = "mypy-0.930-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a9d8dffefba634b27d650e0de2564379a1a367e2e08d6617d8f89261a3bf63b2"}, 354 | {file = "mypy-0.930-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:b419e9721260161e70d054a15abbd50603c16f159860cfd0daeab647d828fc29"}, 355 | {file = "mypy-0.930-cp37-cp37m-win_amd64.whl", hash = "sha256:601f46593f627f8a9b944f74fd387c9b5f4266b39abad77471947069c2fc7651"}, 356 | {file = "mypy-0.930-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1ea7199780c1d7940b82dbc0a4e37722b4e3851264dbba81e01abecc9052d8a7"}, 357 | {file = "mypy-0.930-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:70b197dd8c78fc5d2daf84bd093e8466a2b2e007eedaa85e792e513a820adbf7"}, 358 | {file = "mypy-0.930-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:5feb56f8bb280468fe5fc8e6f56f48f99aa0df9eed3c507a11505ee4657b5380"}, 359 | {file = "mypy-0.930-cp38-cp38-win_amd64.whl", hash = "sha256:2e9c5409e9cb81049bb03fa1009b573dea87976713e3898561567a86c4eaee01"}, 360 | {file = "mypy-0.930-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:554873e45c1ca20f31ddf873deb67fa5d2e87b76b97db50669f0468ccded8fae"}, 361 | {file = "mypy-0.930-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0feb82e9fa849affca7edd24713dbe809dce780ced9f3feca5ed3d80e40b777f"}, 362 | {file = "mypy-0.930-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:bc1a0607ea03c30225347334af66b0af12eefba018a89a88c209e02b7065ea95"}, 363 | {file = "mypy-0.930-cp39-cp39-win_amd64.whl", hash = "sha256:f9f665d69034b1fcfdbcd4197480d26298bbfb5d2dfe206245b6498addb34999"}, 364 | {file = "mypy-0.930-py3-none-any.whl", hash = "sha256:bf4a44e03040206f7c058d1f5ba02ef2d1820720c88bc4285c7d9a4269f54173"}, 365 | {file = "mypy-0.930.tar.gz", hash = "sha256:51426262ae4714cc7dd5439814676e0992b55bcc0f6514eccb4cf8e0678962c2"}, 366 | ] 367 | mypy-extensions = [ 368 | {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, 369 | {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, 370 | ] 371 | packaging = [ 372 | {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"}, 373 | {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, 374 | ] 375 | pathspec = [ 376 | {file = "pathspec-0.9.0-py2.py3-none-any.whl", hash = "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a"}, 377 | {file = "pathspec-0.9.0.tar.gz", hash = "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1"}, 378 | ] 379 | platformdirs = [ 380 | {file = "platformdirs-2.4.0-py3-none-any.whl", hash = "sha256:8868bbe3c3c80d42f20156f22e7131d2fb321f5bc86a2a345375c6481a67021d"}, 381 | {file = "platformdirs-2.4.0.tar.gz", hash = "sha256:367a5e80b3d04d2428ffa76d33f124cf11e8fff2acdaa9b43d545f5c7d661ef2"}, 382 | ] 383 | pluggy = [ 384 | {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, 385 | {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, 386 | ] 387 | py = [ 388 | {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, 389 | {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, 390 | ] 391 | pycodestyle = [ 392 | {file = "pycodestyle-2.8.0-py2.py3-none-any.whl", hash = "sha256:720f8b39dde8b293825e7ff02c475f3077124006db4f440dcbc9a20b76548a20"}, 393 | {file = "pycodestyle-2.8.0.tar.gz", hash = "sha256:eddd5847ef438ea1c7870ca7eb78a9d47ce0cdb4851a5523949f2601d0cbbe7f"}, 394 | ] 395 | pyflakes = [ 396 | {file = "pyflakes-2.4.0-py2.py3-none-any.whl", hash = "sha256:3bb3a3f256f4b7968c9c788781e4ff07dce46bdf12339dcda61053375426ee2e"}, 397 | {file = "pyflakes-2.4.0.tar.gz", hash = "sha256:05a85c2872edf37a4ed30b0cce2f6093e1d0581f8c19d7393122da7e25b2b24c"}, 398 | ] 399 | pyparsing = [ 400 | {file = "pyparsing-3.0.6-py3-none-any.whl", hash = "sha256:04ff808a5b90911829c55c4e26f75fa5ca8a2f5f36aa3a51f68e27033341d3e4"}, 401 | {file = "pyparsing-3.0.6.tar.gz", hash = "sha256:d9bdec0013ef1eb5a84ab39a3b3868911598afa494f5faa038647101504e2b81"}, 402 | ] 403 | pytest = [ 404 | {file = "pytest-6.2.5-py3-none-any.whl", hash = "sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134"}, 405 | {file = "pytest-6.2.5.tar.gz", hash = "sha256:131b36680866a76e6781d13f101efb86cf674ebb9762eb70d3082b6f29889e89"}, 406 | ] 407 | sh = [ 408 | {file = "sh-1.14.2-py2.py3-none-any.whl", hash = "sha256:4921ac9c1a77ec8084bdfaf152fe14138e2b3557cc740002c1a97076321fce8a"}, 409 | {file = "sh-1.14.2.tar.gz", hash = "sha256:9d7bd0334d494b2a4609fe521b2107438cdb21c0e469ffeeb191489883d6fe0d"}, 410 | ] 411 | toml = [ 412 | {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, 413 | {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, 414 | ] 415 | tomli = [ 416 | {file = "tomli-1.2.3-py3-none-any.whl", hash = "sha256:e3069e4be3ead9668e21cb9b074cd948f7b3113fd9c8bba083f48247aab8b11c"}, 417 | {file = "tomli-1.2.3.tar.gz", hash = "sha256:05b6166bff487dc068d322585c7ea4ef78deed501cc124060e0f238e89a9231f"}, 418 | ] 419 | typing-extensions = [ 420 | {file = "typing_extensions-4.0.1-py3-none-any.whl", hash = "sha256:7f001e5ac290a0c0401508864c7ec868be4e701886d5b573a9528ed3973d9d3b"}, 421 | {file = "typing_extensions-4.0.1.tar.gz", hash = "sha256:4ca091dea149f945ec56afb48dae714f21e8692ef22a395223bcd328961b6a0e"}, 422 | ] 423 | --------------------------------------------------------------------------------