Add new portable builder for macos + rewrite all (#2528)

pull/2541/head
patryk4815 1 year ago committed by GitHub
parent 066352a37b
commit 1bafe22ae1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -41,6 +41,12 @@
);
pkgUtil = forAllSystems (system: import ./nix/bundle/pkg.nix { pkgs = pkgsBySystem.${system}; });
portableDrvLldb =
system:
import ./nix/portable.nix {
pkgs = pkgsBySystem.${system};
pwndbg = self.packages.${system}.pwndbg-lldb;
};
portableDrv =
system:
import ./nix/portable.nix {
@ -60,6 +66,7 @@
);
tarballDrv = system: {
tarball = pkgUtil.${system}.buildPackageTarball { drv = portableDrv system; };
tarball-lldb = pkgUtil.${system}.buildPackageTarball { drv = portableDrvLldb system; };
};
in
{

@ -1,178 +0,0 @@
#!/usr/bin/env bash
# Original file copied from https://github.com/3noch/nix-bundle-exe
# But it was modified/patched for pwndbg usecase!
set -euo pipefail
out="$1"
binary="$2"
: "${bin_dir:-}"
: "${lib_dir:-}"
: "${exe_dir:-}"
# Converts paths like "folder/bin" to "../.."
relative_bin_to_lib=$(echo -n "$bin_dir" | sed 's|[^/]*|..|g')
clean_path() {
echo -n "$1" | sed 's#//*#/#g'
}
printNeeded() {
print-needed-elf "$1" | grep '/nix/store/'
}
finalizeBin() {
nuke-refs "$1"
}
relpathPathPrint() {
relative-path "$1" "$2"
}
bundleLib() {
local file="$1"
local install_dir="$out/$2"
mkdir -p $install_dir
local real_file
real_file=$(realpath "$file")
local file_name
file_name=$(basename "$file")
local real_file_name
real_file_name=$(basename "$real_file")
local copied_file
copied_file="$install_dir/$real_file_name"
local already_bundled="1"
if [ ! -f "$copied_file" ]; then
already_bundled="0"
cp "$real_file" "$copied_file"
chmod +w "$copied_file"
fi
if [ "$file_name" != "$real_file_name" ] && [ ! -f "$install_dir/$file_name" ]; then
(cd "$install_dir" && ln -sf "$real_file_name" "$file_name")
chmod +w "$install_dir/$file_name"
fi
if [ "$already_bundled" = "1" ]; then
return
fi
echo "Bundling $real_file to $install_dir"
local linked_libs
linked_libs=$(printNeeded "$real_file" || true)
for linked_lib in $linked_libs; do
bundleLib "$linked_lib" "lib"
done
if [ -n "$linked_libs" ]; then
relative_any_to_lib=$(relpathPathPrint "$out/$lib_dir" "$copied_file")
rpath=$(clean_path "\$ORIGIN/$relative_any_to_lib/$lib_dir")
patchelf --set-rpath "$rpath" "$copied_file"
fi
finalizeBin "$copied_file"
}
bundleExe() {
local exe="$1"
local interpreter="$2"
local exe_name
exe_name=$(basename "$exe")
local copied_exe="$out/$exe_dir/$exe_name"
cp "$exe" "$copied_exe"
chmod +w "$copied_exe"
local rpath
rpath=$(clean_path "\$ORIGIN/$relative_bin_to_lib/$lib_dir")
patchelf --set-interpreter "$(basename "$interpreter")" --set-rpath "$rpath" "$copied_exe"
finalizeBin "$copied_exe"
bundleLib "$interpreter" "lib"
local linked_libs
linked_libs=$(printNeeded "$exe" || true)
for linked_lib in $linked_libs; do
bundleLib "$linked_lib" "lib"
done
# shellcheck disable=SC2016
printf '#!/bin/sh
set -eu
dir="$(cd -- "$(dirname "$(dirname "$(realpath "$0")")")" >/dev/null 2>&1 ; pwd -P)"
exec "$dir"/%s "$dir"/%s "$@"' \
"'$lib_dir/$(basename "$interpreter")'" \
"'$exe_dir/$exe_name'" \
> "$out/$bin_dir/$exe_name"
chmod +x "$out/$bin_dir/$exe_name"
}
remove_prefix() {
local full_path="$1"
local dynamic_prefix="$2"
echo "${full_path#$dynamic_prefix}"
}
bundleCustom() {
local from_dir="$1"
local to_dir="$out/$2"
local files
files=$(find -L "$from_dir" -type f -regex '.*\(\.py\|\.pth\|\.asm\|__doc__\)$' || true)
local real_file_dir
local real_file_name
local install_dir
local install_path
for real_file in $files; do
real_file_dir=$(dirname "$(remove_prefix "$real_file" "$from_dir")")
real_file_name=$(basename "$real_file")
install_dir="$to_dir/$real_file_dir"
install_path="$install_dir/$real_file_name"
mkdir -p $install_dir
echo "Copy $real_file to $install_dir"
# TODO: check symlink like in bundleLib
if [ ! -f "$install_path" ]; then
cp "$real_file" "$install_path"
chmod +w "$install_path"
fi
done
}
bundleDirLib() {
local from_dir="$1"
local linked_libs_root
local path_dir
local linked_libs
bundleCustom "$from_dir" "lib"
linked_libs_root=$(find -L "$from_dir" -type f -regex '.*\.so\(\..*\|$\)' || true)
for linked_lib_root in $linked_libs_root; do
path_dir=$(dirname "$(remove_prefix "$linked_lib_root" "$from_dir")")
bundleLib "$linked_lib_root" "lib/$path_dir"
linked_libs=$(printNeeded "$linked_lib_root" || true)
for linked_lib in $linked_libs; do
bundleLib "$linked_lib" "lib"
done
done
}
exe_interpreter=$(patchelf --print-interpreter "$binary" 2> /dev/null || true)
if [ -n "$exe_interpreter" ]; then
mkdir -p "$out/$exe_dir" "$out/$bin_dir" "$out/$lib_dir"
bundleExe "$binary" "$exe_interpreter"
else
mkdir -p "$out/$exe_dir" "$out/$bin_dir" "$out/$lib_dir"
bundleDirLib "$binary"
fi

@ -1,71 +0,0 @@
#!/usr/bin/env bash
# Original file copied from https://github.com/3noch/nix-bundle-exe
# But it was modified/patched for pwndbg usecase!
set -euo pipefail
echo "darwin is not supported TODO, https://github.com/3noch/nix-bundle-exe/blob/main/bundle-macos.sh"
exit 1
#
#out="$1"
#binary="$2"
#
#: "${bin_dir:-}"
#: "${lib_dir:-}"
#
## Converts paths like "folder/bin" to "../.."
#relative_bin_to_lib=$(echo -n "$bin_dir" | sed 's|[^/]*|..|g')
#
#mkdir -p "$out/$bin_dir" "$out/$lib_dir"
#
#clean_path() {
# echo -n "$1" | sed 's#//*#/#g'
#}
#
#printNeeded() {
# otool -L "$1" | tail -n +2 | grep '/nix/store/' | cut -d '(' -f -1
#}
#
#finalizeBin() {
# nuke-refs "$1"
# codesign -f -s - "$1" || true
#}
#
#bundleBin() {
# local file="$1"
# local file_type="$2"
#
# local real_file
# real_file=$(realpath "$file")
# local install_dir="$out/$lib_dir"
# local rpath_prefix="@loader_path"
# if [ "$file_type" == "exe" ]; then
# install_dir="$out/$bin_dir"
# rpath_prefix=$(clean_path "@executable_path/$relative_bin_to_lib/$lib_dir")
# fi
#
# local copied_file
# copied_file="$install_dir/$(basename "$real_file")"
# if [ -f "$copied_file" ]; then
# return
# fi
#
# echo "Bundling $real_file to $install_dir"
# cp "$real_file" "$copied_file"
# chmod +w "$copied_file"
#
# local linked_libs
# linked_libs=$(printNeeded "$real_file" || true)
# for linked_lib in $linked_libs; do
# local real_lib
# real_lib=$(realpath "$linked_lib")
# local real_lib_name
# real_lib_name=$(basename "$real_lib")
# install_name_tool -change "$linked_lib" "$rpath_prefix/$real_lib_name" "$copied_file"
# bundleBin "$real_lib" "lib"
# done
#
# finalizeBin "$copied_file"
#}
#
#bundleBin "$binary" "exe"

@ -0,0 +1,419 @@
import stat
import subprocess
import shutil
import os
import os.path
import typing
import sys
from pathlib import Path
def check_file_type(file_path: Path) -> str | None:
with open(str(file_path), 'rb') as f:
header = f.read(4)
if header == b'\x7fELF':
return "ELF"
elif header == b'\xfe\xed\xfa\xce':
return "Mach-O 32-bit (Little Endian)"
elif header == b'\xfe\xed\xfa\xcf':
return "Mach-O 64-bit (Little Endian)"
elif header == b'\xce\xfa\xed\xfe':
return "Mach-O 32-bit (Big Endian)"
elif header == b'\xcf\xfa\xed\xfe':
return "Mach-O 64-bit (Big Endian)"
elif header == b'\xca\xfe\xba\xbe':
return "Mach-O Fat Binary (Universal, Little Endian)"
elif header == b'\xbe\xba\xfe\xca':
return "Mach-O Fat Binary (Universal, Big Endian)"
else:
return None
def eprint(msg: str):
print(msg, file=sys.stderr)
def run(args: typing.List[str], no_error=False) -> str:
result = subprocess.run(args, capture_output=True)
if result.returncode != 0:
if no_error:
eprint(result.stderr)
eprint("WARNING: Command failed with return code {}: {}".format(result.returncode, args))
return ''
eprint(result.stderr)
eprint("Command failed with return code {}: {}".format(result.returncode, args))
sys.exit(result.returncode)
return result.stdout.decode("utf-8")
def iter_macho_deps(binary_path: Path) -> typing.Iterator[Path]:
for line in run(["otool", "-L", str(binary_path)]).splitlines():
line = line.strip()
if not line.startswith('/nix/store/'):
continue
splited = line.split(' (', 1)
if len(splited) != 2:
continue
lib_path = Path(splited[0])
if not lib_path.exists():
eprint(f'WARNING: skipping not exists file={lib_path}')
continue
yield lib_path
def iter_elf_deps(binary_path: Path) -> typing.Iterator[Path]:
def stripped_strs(strs: typing.Iterable[str]) -> typing.Iterable[str]:
return (cleaned for x in strs for cleaned in [x.strip()] if cleaned != "")
def get_rpaths(exe: str) -> typing.Iterable[str]:
return stripped_strs(run(["patchelf", "--print-rpath", exe]).split(":"))
def resolve_origin(origin: str, paths: typing.Iterable[str]) -> typing.Iterable[str]:
return (path.replace("$ORIGIN", origin) for path in paths)
def get_needed(exe: str) -> typing.Iterable[str]:
return stripped_strs(run(["patchelf", "--print-needed", exe]).splitlines())
def resolve_paths(needed: typing.Iterable[str], rpaths: typing.List[str]) -> typing.Iterable[str]:
existing_paths = lambda lib, paths: (
abs_path for path in paths for abs_path in [os.path.join(path, lib)]
if os.path.exists(abs_path)
)
for lib in needed:
for found in [next(existing_paths(lib, rpaths), None)]:
if found is None:
eprint(f"WARNING: can't find {lib} in {rpaths}")
continue
yield found
dirname = os.path.dirname(str(binary_path))
rpaths_raw = list(get_rpaths(str(binary_path)))
rpaths_raw = [dirname] if rpaths_raw == [] else rpaths_raw
rpaths = list(resolve_origin(dirname, rpaths_raw))
for path in (x for x in resolve_paths(get_needed(str(binary_path)), rpaths) if x is not None):
if not path.startswith('/nix/store/'):
continue
yield Path(path)
if sys.platform == 'darwin':
iter_deps = iter_macho_deps
else:
iter_deps = iter_elf_deps
def iter_deps_recursive(binary_path: Path, depth: int=None, visited: typing.Set[Path]=None) -> typing.Iterator[Path]:
is_first = depth is None
if depth is None:
depth = 0
if visited is None:
visited = set()
if depth > 20:
raise ValueError(f'depth exceeded {depth}')
binary_path = Path(os.path.normpath(binary_path))
if binary_path in visited:
return
visited.add(binary_path)
if not is_first:
yield binary_path
for dep in iter_deps(binary_path):
yield from iter_deps_recursive(dep, depth=depth + 1, visited=visited)
def iter_dir_recursive(dir_path: Path, depth: int = None, visited: typing.Set[Path] = None) -> typing.Iterator[
typing.Tuple[Path, typing.List[Path]]]:
if depth is None:
depth = 0
if visited is None:
visited = set()
if depth > 20:
raise ValueError(f'depth exceeded {depth}')
if dir_path in visited:
return
visited.add(dir_path)
stored_dirs = []
stored_files = []
for entry in dir_path.iterdir():
if entry.is_dir():
stored_dirs.append(entry)
elif entry.is_file():
stored_files.append(entry)
else:
eprint(f"WARNING: Unrecognized entry {entry}")
continue
yield dir_path, stored_files
del stored_files
for subdir in stored_dirs:
yield from iter_dir_recursive(subdir, depth=depth + 1, visited=visited)
def cleanup_nixrefs(binary_path: Path):
# Modify the binary to replace references to actual Nix store paths (e.g., /nix/store/valid-hash)
# with invalid or placeholder paths (e.g., /nix/store/invalid-hash), ensuring the binary
# doesnt inadvertently depend on specific Nix store contents.
run(['nuke-refs', str(binary_path)])
if sys.platform == 'darwin':
# Force an "ad-hoc" code signature on the binary (using '-' as the identity placeholder).
# This is typically used to satisfy macOS code signing requirements without a valid signing certificate.
# The `-f` option forces re-signing if the binary is already signed.
run(['codesign', '-f', '-s', '-', str(binary_path)], no_error=True)
def patch_library_macho(binary_path: Path, root_dst: Path, *, is_exe: bool):
lib_dir = root_dst / 'lib'
if is_exe:
# For executable files (e.g., /abs/exe/gdb), replace absolute library paths with paths relative to the executable.
# Example: replace /abs/lib/libLLVM.dylib with @executable_path/../lib/libLLVM.dylib
# This makes the executable locate libraries in its own relative directory structure at runtime.
prefix_lib = '@executable_path/'
else:
# For shared libraries (e.g., /abs/lib/python3.12/capstone/foo.dylib), replace absolute library paths with paths relative to the library.
# Example: replace /abs/lib/libiconv.2.dylib with @loader_path/../../libiconv.2.dylib
# This allows libraries to locate dependencies in a relative directory structure without absolute paths.
prefix_lib = '@loader_path/'
# When `binary_path` is already patched. `iter_deps` should return empty list
for src_lib_path in iter_deps(binary_path):
dst_lib_path = lib_dir / src_lib_path.name
rel_path = os.path.relpath(dst_lib_path, binary_path.parent)
print(f'Patching {binary_path.name}: {src_lib_path.name}->{rel_path}')
run(["install_name_tool", "-change", str(src_lib_path), prefix_lib + rel_path, str(binary_path)])
cleanup_nixrefs(binary_path)
def patch_library_elf(binary_path: Path, root_dst: Path, *, is_exe: bool):
# Ensure that $ORIGIN resolves relative to the actual binary's resolved location,
# not the symlink's location.
#
# Using symlinks can cause issues, for example:
# lib/python3.12/site-packages/lldb/_lldb.cpython-312-aarch64-linux-gnu.so -> ../../../liblldb.so.19.1.1
#
# On Linux, $ORIGIN is resolved based on the location of the symlink itself,
# not the resolved target location of the binary. This behavior can lead to
# runtime errors if the symlink points to a path outside the expected structure.
#
# On macOS, the equivalent mechanism `@loader_path` correctly (sic!) resolves relative
# to the binary's actual location, even when symlinks are involved.
#
# To maintain compatibility and avoid such issues, symlinks should be avoided
# in scenarios where $ORIGIN is used.
prefix_lib = '$ORIGIN/'
rel_path = Path(os.path.relpath(root_dst, binary_path.parent)) / 'lib'
rpath = prefix_lib + str(rel_path)
print(f'Patching {binary_path.name}')
# When `binary_path` is already patched. `iter_deps` should return empty list
# We need to be sure to not patch ld-loader or libc
is_rpath_patch_needed = bool(next(iter_deps(binary_path), None))
if is_rpath_patch_needed:
if is_exe:
interpreter_path = Path(run(["patchelf", "--print-interpreter", str(binary_path)]).strip())
run(["patchelf", "--set-interpreter", interpreter_path.name, "--set-rpath", rpath, str(binary_path)])
else:
run(["patchelf", "--set-rpath", rpath, str(binary_path)])
cleanup_nixrefs(binary_path)
if sys.platform == 'darwin':
patch_library = patch_library_macho
else:
patch_library = patch_library_elf
def copy_with_chmod(src: Path, dst: Path):
if os.path.isdir(dst):
raise ValueError('only coping file supported ;)')
if not dst.parent.exists():
dst.parent.mkdir(parents=True, exist_ok=True)
shutil.copy(src, dst)
# add writable
dst.lchmod(dst.stat().st_mode | stat.S_IWUSR)
def symlink(target: Path | str, dst: Path):
if os.path.isdir(dst):
raise ValueError('only coping file supported ;)')
if not dst.parent.exists():
dst.parent.mkdir(parents=True, exist_ok=True)
dst.symlink_to(str(target))
def copy_with_symlink_normal(src_file_path: Path, root_dir_src: Path, root_dst_dir: Path, is_so: bool=False) -> Path | None:
dst_file_path = root_dst_dir / src_file_path.relative_to(root_dir_src)
if dst_file_path.exists():
return dst_file_path
if src_file_path.is_symlink():
file_resolved = src_file_path.resolve()
is_allowed_symlink = file_resolved.is_relative_to(root_dir_src)
if is_so and is_allowed_symlink:
# For .so / .dylib files, symlinks are only allowed within the same directory.
# This is because $ORIGIN in the runpath cannot resolve symlinks.
# This issue was specifically encountered with the file:
# lib/python3.12/site-packages/lldb/_lldb.cpython-312-aarch64-linux-gnu.so -> ../../../liblldb.so.19.1.1
# To avoid such issues, we check if the resolved file's parent directory
# matches the parent directory of the source file.
if file_resolved.relative_to(root_dir_src).parent != src_file_path.parent:
is_allowed_symlink = False
if is_allowed_symlink:
# symlinked-file.txt should points to relative ../../original-file.txt
# Allowed to create symlink, because they are under same root
rel_path = os.path.relpath(file_resolved, src_file_path.parent)
print(f'CopyingSym {dst_file_path}->{rel_path}')
symlink(target=rel_path, dst=dst_file_path)
new_real_dst = root_dst_dir / file_resolved.relative_to(root_dir_src)
if new_real_dst.exists():
return new_real_dst
print(f'Copying {src_file_path.name} to {new_real_dst.parent}')
copy_with_chmod(src_file_path, new_real_dst)
return new_real_dst
else:
# hard copy file without symlink, because they are in different root
pass
print(f'Copying {src_file_path.name} to {dst_file_path.parent}')
copy_with_chmod(src_file_path, dst_file_path)
return dst_file_path
def copy_with_symlink_lib(src_path: Path, dst_dir: Path) -> Path | None:
new_file = dst_dir / src_path.name
if new_file.exists():
return new_file
if src_path.is_symlink():
src_resolved_lib_path = src_path.resolve()
is_weird_symlink = src_resolved_lib_path.name == src_path.name
if is_weird_symlink:
eprint(f'WARNING: Shouldn\'t happen? {src_path}->{src_resolved_lib_path}, coping file')
print(f'Bundling {src_path.name} to {new_file.parent}')
copy_with_chmod(src_path, new_file)
return new_file
symlink_path = dst_dir / src_path.name
print(f'BundlingSym {symlink_path.name}->{src_resolved_lib_path.name} to {symlink_path.parent}')
symlink(target=src_resolved_lib_path.name, dst=symlink_path)
new_file = dst_dir / src_resolved_lib_path.name
if new_file.exists():
return new_file
print(f'Bundling {src_resolved_lib_path.name} to {new_file.parent}')
copy_with_chmod(src_resolved_lib_path, new_file)
return new_file
else:
print(f'Bundling {src_path.name} to {new_file.parent}')
copy_with_chmod(src_path, new_file)
return new_file
def bundle_library(binary_path: Path, root_dst: Path, *, is_exe: bool, dst_path: Path=None):
lib_dir = root_dst / 'lib'
exe_dir = root_dst / 'exe'
if not binary_path.is_relative_to(root_dst):
# coping required, because src-binary and dst-binary are in different roots
binary_path = copy_with_symlink_lib(binary_path, exe_dir if is_exe else lib_dir)
# Move file to another place
if is_exe and dst_path:
shutil.move(binary_path, dst_path)
binary_path = dst_path
# Store all needed libs into {root}/lib/*
for src_lib_path in iter_deps_recursive(binary_path):
real_file = copy_with_symlink_lib(src_lib_path, lib_dir)
if real_file is None:
continue
patch_library(real_file, root_dst, is_exe=False)
# fix main
patch_library(binary_path, root_dst, is_exe=is_exe)
def bundle_python_venv(src_lib_dir: Path, out_lib_dir: Path, root_dst: Path):
bundle_binaries = set()
for _, files in iter_dir_recursive(src_lib_dir):
for src_file_path in files:
# search for so files:
# - /libpython3.12.so.1.0
# - /libpython3.12.so
# - /libpython3.12.dylib
is_so = any(suffix in src_file_path.suffixes for suffix in (
'.so',
'.dylib',
))
is_good_ext = src_file_path.suffix in (
'.py', # python script file
'.pyi', '.typed', # python types
'.asm', # pwntools asm templates
)
is_good_name = src_file_path.name in (
'__doc__', # pwntools asm templates
)
if not (is_so or is_good_ext or is_good_name):
continue
real_file = copy_with_symlink_normal(src_file_path, src_lib_dir, out_lib_dir, is_so=is_so)
if is_so and real_file:
bundle_binaries.add(real_file)
for file in bundle_binaries:
bundle_library(file, root_dst, is_exe=False)
def main():
out = Path(sys.argv[1])
rest_argv = sys.argv[2:]
for src_path, dst_part in zip(rest_argv[::2], rest_argv[1::2]):
is_dir = str(dst_part).endswith('/')
src_path = Path(src_path)
dst_part = Path(dst_part)
dst_path = out / dst_part
if is_dir:
bundle_python_venv(src_path, dst_path, out)
else:
if check_file_type(src_path):
bundle_library(src_path, out, is_exe=True, dst_path=dst_path)
else:
copy_with_chmod(src_path, dst_path)
main()

@ -1,10 +1,7 @@
{
pkgs,
bin_dir ? "bin",
exe_dir ? "exe",
lib_dir ? if pkgs.stdenv.isDarwin then "Frameworks/Library.dylib" else "lib",
}:
path:
paths:
# Original file copied from https://github.com/3noch/nix-bundle-exe
# But it was modified/patched for pwndbg usecase!
# May be:
@ -12,48 +9,27 @@ path:
# 2) a path to a directory containing bin/, or
# 3) a path to an executable.
let
print-needed-elf = pkgs.writeScriptBin "print-needed-elf" '''${pkgs.python3}'/bin/python ${./print_needed_elf.py} "$@"'';
relative-path = pkgs.writeScriptBin "relative-path" '''${pkgs.python3}'/bin/python ${./relative-path.py} "$@"'';
cfg =
deps =
if pkgs.stdenv.isDarwin then
{
deps = with pkgs; [
darwin.binutils
darwin.sigtool
];
script = "bash ${./bundle-macos.sh}";
}
[
pkgs.darwin.cctools
pkgs.darwin.binutils
pkgs.darwin.sigtool
]
else if pkgs.stdenv.isLinux then
{
deps = [
pkgs.glibc
print-needed-elf
relative-path
];
script = "bash ${./bundle-linux.sh}";
}
[
pkgs.patchelf
]
else
throw "Unsupported platform: only darwin and linux are supported";
name = if pkgs.lib.isDerivation path then path.name else builtins.baseNameOf path;
overrideEnv = name: value: if value == null then "" else "export ${name}='${value}'";
in
pkgs.runCommand "bundle-${name}" { nativeBuildInputs = cfg.deps ++ [ pkgs.nukeReferences ]; } ''
pkgs.runCommand "pwndbg-bundler" {
nativeBuildInputs = deps ++ [
pkgs.nukeReferences
pkgs.python3
];
} ''
set -euo pipefail
export bin_dir='${bin_dir}'
export exe_dir='${exe_dir}'
export lib_dir='${lib_dir}'
${
if builtins.pathExists "${path}/bin" then
''
find '${path}/bin' -type f -executable -print0 | xargs -0 --max-args 1 ${cfg.script} "$out"
''
else
''
${cfg.script} "$out" ${pkgs.lib.escapeShellArg path}
''
}
python3 ${./bundle.py} "$out" ${pkgs.lib.escapeShellArgs paths}
find $out -empty -type d -delete
''

@ -10,6 +10,9 @@ let
"armv7l-linux" = "armv7";
"riscv64-linux" = "riscv64";
"aarch64-darwin" = "macos_arm64";
"x86_64-darwin" = "macos_amd64";
};
buildPackagePFPM =

@ -1,63 +0,0 @@
#!/usr/bin/env python
# Original file copied from https://github.com/3noch/nix-bundle-exe
# But it was modified/patched for pwndbg usecase!
# Prints resolved paths to needed libraries for an ELF executable.
# ldd also does this, but it segfaults in some odd scenarios so we avoid it.
import sys
import os
import subprocess
from typing import Any, Iterable, List
def eprint(msg: Any):
print(msg, file=sys.stderr)
def run(args: List[str]) -> str:
result = subprocess.run(args, capture_output=True)
if result.returncode != 0:
eprint(result.stderr)
eprint("Command failed with return code {}: {}".format(result.returncode, args))
sys.exit(result.returncode)
return result.stdout.decode("utf-8")
def stripped_strs(strs: Iterable[str]) -> Iterable[str]:
return (cleaned for x in strs for cleaned in [x.strip()] if cleaned != "")
def get_rpaths(exe: str) -> Iterable[str]:
return stripped_strs(run(["patchelf", "--print-rpath", exe]).split(":"))
def resolve_origin(origin: str, paths: Iterable[str]) -> Iterable[str]:
return (path.replace("$ORIGIN", origin) for path in paths)
def get_needed(exe: str) -> Iterable[str]:
return stripped_strs(run(["patchelf", "--print-needed", exe]).splitlines())
def resolve_paths(needed: Iterable[str], rpaths: List[str]) -> Iterable[str]:
existing_paths = lambda lib, paths: (
abs_path for path in paths for abs_path in [os.path.join(path, lib)]
if os.path.exists(abs_path)
)
return (
found if found is not None else eprint("Warning: can't find {} in {}".format(lib, rpaths))
for lib in needed for found in [next(existing_paths(lib, rpaths), None)]
)
def main(exe: str):
dirname = os.path.dirname(exe)
rpaths_raw = list(get_rpaths(exe))
rpaths_raw = [dirname] if rpaths_raw == [] else rpaths_raw
rpaths = list(resolve_origin(dirname, rpaths_raw))
for path in (x for x in resolve_paths(get_needed(exe), rpaths) if x is not None):
print(path)
if __name__ == "__main__":
main(*sys.argv[1:])

@ -1,4 +0,0 @@
import os.path
import sys
print(os.path.relpath(*sys.argv[1:]))

@ -3,25 +3,80 @@
pwndbg ? import ./pwndbg.nix { },
}:
let
isLLDB = pwndbg.meta.isLLDB;
lldb = pwndbg.meta.lldb;
gdb = pwndbg.meta.gdb;
python3 = pwndbg.meta.python3;
pwndbgVenv = pwndbg.meta.pwndbgVenv;
gdbBundledLib = pkgs.callPackage ./bundle { } "${gdb}/bin/gdb";
pyEnvBundledLib = pkgs.callPackage ./bundle { } "${pwndbgVenv}/lib/";
bundler = arg: (pkgs.callPackage ./bundle { } arg);
ldName = pkgs.lib.readFile (
pkgs.runCommand "bundle" { nativeBuildInputs = [ pkgs.patchelf ]; } ''
echo -n $(patchelf --print-interpreter "${gdbBundledLib}/exe/gdb") > $out
pkgs.runCommand "pwndbg-bundle-ld-name-IFD" { nativeBuildInputs = [ pkgs.patchelf ]; } ''
echo -n $(basename $(patchelf --print-interpreter "${gdb}/bin/gdb")) > $out
''
);
ldLoader = if pkgs.stdenv.isDarwin then "" else "\"$dir/lib/${ldName}\"";
pwndbgBundleBin = pkgs.writeScript "pwndbg" ''
linuxLldbEnvs = pkgs.lib.optionalString (pkgs.stdenv.isLinux && isLLDB) ''
export LLDB_DEBUGSERVER_PATH="$dir/bin/lldb-server"
'';
wrapperBinPwndbgGdbinit = pkgs.writeScript "pwndbg-wrapper-bin-gdbinit" ''
#!/bin/sh
dir="$(cd -- "$(dirname "$(dirname "$(realpath "$0")")")" >/dev/null 2>&1 ; pwd -P)"
export PYTHONHOME="$dir"
export PATH="$dir/bin/:$PATH"
exec ${ldLoader} "$dir/exe/gdb" --quiet --early-init-eval-command="set auto-load safe-path /" --command=$dir/exe/gdbinit.py "$@"
'';
wrapperBinPy = file: pkgs.writeScript "pwndbg-wrapper-bin-py" ''
#!/bin/sh
dir="$(cd -- "$(dirname "$(dirname "$(realpath "$0")")")" >/dev/null 2>&1 ; pwd -P)"
export PYTHONHOME="$dir"
export PATH="$dir/bin/:$PATH"
${linuxLldbEnvs}
exec ${ldLoader} "$dir/exe/python3" "$dir/${file}" "$@"
'';
wrapperBin = file: pkgs.writeScript "pwndbg-wrapper-bin" ''
#!/bin/sh
dir="$(cd -- "$(dirname "$(dirname "$(realpath "$0")")")" >/dev/null 2>&1 ; pwd -P)"
export PATH="$dir/bin/:$PATH"
export PYTHONHOME="$dir"
exec "$dir/lib/${ldName}" "$dir/exe/gdb" --quiet --early-init-eval-command="set auto-load safe-path /" --command=$dir/exe/gdbinit.py "$@"
${linuxLldbEnvs}
exec ${ldLoader} "$dir/${file}" "$@"
'';
skipVenv = pkgs.writeScript "pwndbg-skip-venv" "";
pwndbgGdbBundled = bundler [
"${pkgs.lib.getBin gdb}/bin/gdb" "exe/gdb"
"${pkgs.lib.getBin gdb}/bin/gdbserver" "exe/gdbserver"
"${gdb}/share/gdb/" "share/gdb/"
"${pwndbgVenv}/lib/" "lib/"
"${pwndbg.src}/pwndbg/" "lib/${python3.libPrefix}/site-packages/pwndbg/"
"${pwndbg.src}/gdbinit.py" "exe/gdbinit.py"
"${skipVenv}" "exe/.skip-venv"
"${wrapperBinPwndbgGdbinit}" "bin/pwndbg"
"${wrapperBin "exe/gdbserver"}" "bin/gdbserver"
];
pwndbgLldbBundled = bundler [
"${pkgs.lib.getBin lldb}/bin/.lldb-wrapped" "exe/lldb"
"${pkgs.lib.getBin lldb}/bin/lldb-server" "exe/lldb-server"
"${pkgs.lib.getLib lldb}/lib/" "lib/"
"${pwndbgVenv}/lib/" "lib/"
"${python3}/bin/python3" "exe/python3"
"${pwndbg.src}/pwndbg/" "lib/${python3.libPrefix}/site-packages/pwndbg/"
"${pwndbg.src}/lldbinit.py" "exe/lldbinit.py"
"${pwndbg.src}/pwndbg-lldb.py" "exe/pwndbg-lldb.py"
"${skipVenv}" "exe/.skip-venv"
"${wrapperBin "exe/lldb-server"}" "bin/lldb-server"
"${wrapperBin "exe/lldb"}" "bin/lldb"
"${wrapperBinPy "exe/pwndbg-lldb.py"}" "bin/pwndbg-lldb"
];
pwndbgBundled = if isLLDB then pwndbgLldbBundled else pwndbgGdbBundled;
portable =
pkgs.runCommand "portable-${pwndbg.name}"
@ -33,31 +88,18 @@ let
};
}
''
mkdir -p $out/pwndbg/bin/
mkdir -p $out/pwndbg/lib/
mkdir -p $out/pwndbg/exe/
mkdir -p $out/pwndbg/share/gdb/
touch $out/pwndbg/exe/.skip-venv
cp -rf ${gdbBundledLib}/exe/* $out/pwndbg/exe/
cp -rf ${gdbBundledLib}/lib/* $out/pwndbg/lib/
cp -rf ${pyEnvBundledLib}/lib/* $out/pwndbg/lib/
cp -rf ${pwndbgVenv}/share/gdb/* $out/pwndbg/share/gdb/
cp -rf ${gdb}/share/gdb/* $out/pwndbg/share/gdb/
chmod -R +w $out
mkdir -p $out/pwndbg/
# copy
cp -rf ${pwndbgBundled}/* $out/pwndbg/
cp -rf ${pwndbg.src}/pwndbg $out/pwndbg/lib/${python3.libPrefix}/site-packages/
cp ${pwndbg.src}/gdbinit.py $out/pwndbg/exe/
cp ${pwndbgBundleBin} $out/pwndbg/bin/pwndbg
# writable out
chmod -R +w $out
# fix python "subprocess.py" to use "/bin/sh" and not the nix'ed version, otherwise "gdb-pt-dump" is broken
substituteInPlace $out/pwndbg/lib/${python3.libPrefix}/subprocess.py --replace "'${pkgs.bash}/bin/sh'" "'/bin/sh'"
# build pycache
chmod -R +w $out/pwndbg/lib/${python3.libPrefix}/site-packages/pwndbg
SOURCE_DATE_EPOCH=0 ${pwndbgVenv}/bin/python3 -c "import compileall; compileall.compile_dir('$out', stripdir='$out', force=True);"
SOURCE_DATE_EPOCH=0 ${python3}/bin/python3 -c "import compileall; compileall.compile_dir('$out', stripdir='$out', force=True);"
'';
in
portable

@ -124,6 +124,8 @@ let
pwndbgVenv = pyEnv;
python3 = python3;
gdb = gdb;
lldb = lldb;
isLLDB = isLLDB;
};
};
in

Loading…
Cancel
Save