Skip to content

Commit 175a245

Browse files
committed
feat: add self-extracting stub for platforms without UPX support
1 parent 57e82c2 commit 175a245

5 files changed

Lines changed: 22908 additions & 5 deletions

File tree

.docker/Dockerfile.alpine

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,8 @@ RUN if [ "$ARCH" == "amd64" ] || [ "$ARCH" == "arm64v8" ]; then \
105105

106106
RUN pip3 install --break-system-packages --root-user-action ignore mistletoe
107107

108+
RUN apk add py3-xxhash py3-zstd py3-lz4
109+
108110
# Set up git & user
109111
RUN git config --global --add safe.directory /workspace
110112
RUN adduser -G users -s bash -u 1000 -D mhx

CMakeLists.txt

Lines changed: 53 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -911,6 +911,26 @@ if(STATIC_BUILD_DO_NOT_USE)
911911
foreach(tgt ${BINARY_TARGETS} ${TEST_TARGETS})
912912
target_link_libraries(${tgt} PRIVATE gcc_eh)
913913
endforeach()
914+
915+
if(WITH_UNIVERSAL_BINARY OR WITH_FUSE_EXTRACT_BINARY)
916+
# Build stub for self-extracting universal binary.
917+
add_executable(sfxstub_zstd sfx/stub.c)
918+
target_compile_options(sfxstub_zstd PRIVATE -O2)
919+
920+
if(LIBLZ4_FOUND)
921+
add_executable(sfxstub_lz4 sfx/stub.c)
922+
target_compile_definitions(sfxstub_lz4 PRIVATE DWARFS_SFX_STUB_USE_LZ4=1)
923+
target_link_libraries(sfxstub_lz4 PRIVATE PkgConfig::XXHASH PkgConfig::LIBLZ4)
924+
endif()
925+
926+
foreach(tgt sfxstub_zstd sfxstub_lz4)
927+
if(TARGET ${tgt})
928+
target_compile_options(${tgt} PRIVATE -fno-stack-protector -fno-asynchronous-unwind-tables)
929+
target_link_options(${tgt} PRIVATE -Wl,--build-id=none)
930+
list(APPEND BINARY_TARGETS ${tgt})
931+
endif()
932+
endforeach()
933+
endif()
914934
endif(STATIC_BUILD_DO_NOT_USE)
915935
916936
foreach(tgt ${TEST_TARGETS})
@@ -1040,6 +1060,10 @@ if(STATIC_BUILD_DO_NOT_USE OR WIN32)
10401060
list(APPEND UNIVERSAL_TARGETS dwarfsfuseextract)
10411061
endif()
10421062
1063+
if(${CMAKE_SYSTEM_PROCESSOR} MATCHES "^(i386|x86_64|AMD64|aarch64|ppc64le|arm)$")
1064+
find_program(UPX_EXE upx upx.exe PATHS "c:/bin" DOC "ultimate packer for executables")
1065+
endif()
1066+
10431067
foreach(tgt ${UNIVERSAL_TARGETS})
10441068
get_target_property(TARGET_FILENAME ${tgt} OUTPUT_NAME)
10451069
get_filename_component(TARGET_FILENAME ${TARGET_FILENAME} NAME_WLE)
@@ -1055,10 +1079,6 @@ if(STATIC_BUILD_DO_NOT_USE OR WIN32)
10551079
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
10561080
)
10571081
else()
1058-
if(${CMAKE_SYSTEM_PROCESSOR} MATCHES "^(i386|x86_64|AMD64|aarch64|ppc64le|arm)$")
1059-
find_program(UPX_EXE upx upx.exe PATHS "c:/bin" DOC "ultimate packer for executables")
1060-
endif()
1061-
10621082
if(UPX_EXE)
10631083
# upx -9 is a good compromise between compression ratio and speed
10641084
# also, anything above --best increases the startup time of the compressed
@@ -1068,12 +1088,40 @@ if(STATIC_BUILD_DO_NOT_USE OR WIN32)
10681088
COMMAND ${UPX_EXE} -9 --best -o ${UNIVERSAL_OUT} $<TARGET_FILE:${tgt}>
10691089
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
10701090
)
1091+
elseif(TARGET sfxstub_lz4 OR TARGET sfxstub_zstd)
1092+
# Fallback if UPX is not found or doesn't support the target architecture.
1093+
# Prefer lz4 if available (as it is faster), otherwise use zstd.
1094+
if(TARGET sfxstub_zstd)
1095+
set(_sfx_stub_target sfxstub_zstd)
1096+
# Sweet spot between compression ratio and decompression speed.
1097+
set(_sfx_options "--level=16")
1098+
else()
1099+
set(_sfx_stub_target sfxstub_lz4)
1100+
set(_sfx_options "--lz4")
1101+
endif()
1102+
add_custom_command(
1103+
OUTPUT ${UNIVERSAL_OUT}
1104+
COMMAND ${CMAKE_SOURCE_DIR}/sfx/pack.py
1105+
${_sfx_options}
1106+
--stub $<TARGET_FILE:${_sfx_stub_target}>
1107+
--input $<TARGET_FILE:${tgt}>
1108+
--output ${UNIVERSAL_OUT}
1109+
COMMAND ${CMAKE_CROSSCOMPILING_EMULATOR}
1110+
${UNIVERSAL_OUT} --extract-wrapped-binary ${UNIVERSAL_OUT}.extracted
1111+
COMMAND ${CMAKE_COMMAND} -E compare_files
1112+
${CMAKE_CURRENT_BINARY_DIR}/${UNIVERSAL_OUT}.extracted
1113+
$<TARGET_FILE:${tgt}>
1114+
COMMAND ${CMAKE_COMMAND} -E remove
1115+
${CMAKE_CURRENT_BINARY_DIR}/${UNIVERSAL_OUT}.extracted
1116+
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
1117+
COMMENT "UPX not found/supported, using ${_sfx_stub_target} for universal binary"
1118+
)
10711119
else()
10721120
add_custom_command(
10731121
OUTPUT ${UNIVERSAL_OUT}
10741122
COMMAND ${CMAKE_COMMAND} -E copy $<TARGET_FILE:${tgt}> ${UNIVERSAL_OUT}
10751123
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
1076-
COMMENT "UPX not found, copying binary without compression"
1124+
COMMENT "UPX not found/supported, copying binary without compression"
10771125
)
10781126
endif()
10791127
endif()

sfx/pack.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
#!/usr/bin/env python3
2+
3+
"""
4+
Create a self-extracting, zstd-compressed binary
5+
"""
6+
7+
import argparse
8+
import lz4.block
9+
import os
10+
import shutil
11+
import struct
12+
import subprocess
13+
import sys
14+
import tempfile
15+
import xxhash
16+
import zstd
17+
18+
TRAILER_MAGIC = b"SQUEEZE!"
19+
TRAILER_SIZE = 32
20+
21+
22+
def main():
23+
ap = argparse.ArgumentParser()
24+
ap.add_argument("--stub", required=True)
25+
ap.add_argument("--input", required=True)
26+
ap.add_argument("--output", required=True)
27+
ap.add_argument("--lz4", action="store_true")
28+
ap.add_argument("--level", type=int)
29+
args = ap.parse_args()
30+
31+
if args.level is None:
32+
args.level = 12 if args.lz4 else 19
33+
34+
if not os.path.isfile(args.stub):
35+
ap.error(f"--stub not found: {args.stub}")
36+
if not os.path.isfile(args.input):
37+
ap.error(f"--input not found: {args.input}")
38+
39+
with open(args.input, "rb") as f:
40+
payload = f.read()
41+
42+
u_size = len(payload)
43+
u_xxh64 = xxhash.xxh64(payload).intdigest()
44+
45+
if args.lz4:
46+
compressed = lz4.block.compress(
47+
payload, mode="high_compression", compression=args.level, store_size=False
48+
)
49+
else:
50+
compressed = zstd.compress(payload, args.level)
51+
52+
c_size = len(compressed)
53+
54+
with open(args.stub, "rb") as fstub, open(args.output, "wb") as fout:
55+
shutil.copyfileobj(fstub, fout)
56+
fout.write(compressed)
57+
trailer = TRAILER_MAGIC + struct.pack("<QQQ", u_size, c_size, u_xxh64)
58+
assert len(trailer) == TRAILER_SIZE
59+
fout.write(trailer)
60+
61+
os.chmod(args.output, os.stat(args.output).st_mode | 0o111)
62+
print(
63+
f"OK: wrote {args.output} (orig {u_size} bytes, xxh64=0x{u_xxh64:016x}, payload {c_size} bytes)"
64+
)
65+
66+
67+
if __name__ == "__main__":
68+
main()

0 commit comments

Comments
 (0)