Gate static Clang builds on macOS, Windows and Linux

This commit is contained in:
Max Lv
2026-09-11 09:54:45 +08:00
parent 46f0bddfb9
commit 8d504e6218
6 changed files with 235 additions and 0 deletions
+84
View File
@@ -73,6 +73,90 @@ jobs:
& ./consumer/consumer_shared.exe
if ($LASTEXITCODE -ne 0) { throw "Shared consumer failed: $LASTEXITCODE" }
static-macos:
runs-on: macos-latest
env:
CC: clang
steps:
- uses: actions/checkout@v6
- name: Build Clang static-only installation
run: |
cmake -S . -B build-static -DCMAKE_C_COMPILER=clang -DSS_DEPENDENCY_MODE=bundled -DWITH_STATIC=ON -DSS_BUILD_STATIC_LIBRARY=ON -DSS_BUILD_SHARED_LIBRARY=OFF
cmake --build build-static --parallel 4
ctest --test-dir build-static -L 'unit|vendor' --output-on-failure --no-tests=error
cmake --install build-static --prefix "$PWD/install-static"
mv install-static relocated-static
cmake -S tests/consumer -B consumer-static -DCMAKE_C_COMPILER=clang -DCMAKE_PREFIX_PATH="$PWD/relocated-static"
cmake --build consumer-static
test ! -e consumer-static/consumer_shared
./consumer-static/consumer_static
- name: Reject third-party dynamic libraries
run: python3 tests/check_static_runtime.py relocated-static/bin/ss-* consumer-static/consumer_static
- name: Exercise installed static programs
run: |
python3 tests/interop.py --self --bin relocated-static/bin
python3 tests/interop.py --self --bin relocated-static/bin --plugin tests/sip003_fixture.py
- uses: actions/upload-artifact@v4
with:
name: static-macos-${{ runner.arch }}
path: relocated-static/
if-no-files-found: error
static-windows:
runs-on: windows-latest
defaults:
run:
shell: msys2 {0}
steps:
- uses: actions/checkout@v6
- uses: msys2/setup-msys2@v2
with:
msystem: CLANG64
update: true
install: >-
mingw-w64-clang-x86_64-clang
mingw-w64-clang-x86_64-llvm
mingw-w64-clang-x86_64-cmake
mingw-w64-clang-x86_64-ninja
mingw-w64-clang-x86_64-python
- name: Build Clang static-only installation
run: |
cmake -S . -B build-static -G Ninja -DCMAKE_C_COMPILER=clang -DSS_DEPENDENCY_MODE=bundled -DWITH_STATIC=ON -DSS_BUILD_STATIC_LIBRARY=ON -DSS_BUILD_SHARED_LIBRARY=OFF
cmake --build build-static --parallel 4
ctest --test-dir build-static -L 'unit|vendor' --output-on-failure --no-tests=error
cmake --install build-static --prefix "$PWD/install-static"
mv install-static relocated-static
cmake -S tests/consumer -B consumer-static -G Ninja -DCMAKE_C_COMPILER=clang -DCMAKE_PREFIX_PATH="$PWD/relocated-static"
cmake --build consumer-static
test ! -e consumer-static/consumer_shared.exe
- name: Reject third-party DLL imports, including delay imports
run: python tests/check_static_runtime.py relocated-static/bin/ss-*.exe consumer-static/consumer_static.exe
- name: Exercise installed programs without toolchain DLLs
run: python tests/interop.py --self --bin relocated-static/bin --isolate-windows-runtime
- name: Run static consumer with only Windows system directories on PATH
shell: pwsh
run: |
$env:PATH = "$env:SystemRoot\System32;$env:SystemRoot"
& ./consumer-static/consumer_static.exe
if ($LASTEXITCODE -ne 0) { throw "Static consumer failed: $LASTEXITCODE" }
- uses: actions/upload-artifact@v4
with:
name: static-windows-${{ runner.arch }}
path: relocated-static/
if-no-files-found: error
static-linux:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Build and test fully static Clang/musl executables
run: docker build -f docker/static/Dockerfile --target artifacts --output type=local,dest=static-linux .
- uses: actions/upload-artifact@v4
with:
name: static-linux-${{ runner.arch }}
path: static-linux/
if-no-files-found: error
freebsd:
runs-on: ubuntu-latest
steps:
+33
View File
@@ -0,0 +1,33 @@
ARG ALPINE_IMAGE=alpine:3.24
FROM ${ALPINE_IMAGE} AS build
ARG ALPINE_MIRROR=https://dl-cdn.alpinelinux.org/alpine
RUN sed -i "s|https://dl-cdn.alpinelinux.org/alpine|${ALPINE_MIRROR}|g" /etc/apk/repositories \
&& apk add --no-cache clang lld make musl-dev compiler-rt cmake linux-headers python3 binutils file
ENV CC=clang
WORKDIR /src
COPY . .
ARG SS_MINIMAL=OFF
RUN --network=none cmake -S . -B /build \
-DCMAKE_BUILD_TYPE=Release \
-DSS_DEPENDENCY_MODE=bundled -DWITH_STATIC=ON \
-DSS_BUILD_STATIC_LIBRARY=OFF -DSS_BUILD_SHARED_LIBRARY=OFF \
-DSS_MINIMAL=${SS_MINIMAL} -DCMAKE_EXE_LINKER_FLAGS="-static -fuse-ld=lld --rtlib=compiler-rt" \
-DCMAKE_INSTALL_PREFIX=/out \
&& cmake --build /build --parallel 4 \
&& ctest --test-dir /build -L 'unit|vendor' --output-on-failure --no-tests=error \
&& python3 tests/interop.py --self --bin /build/bin \
&& cmake --install /build --strip \
&& for binary in /out/bin/ss-*; do \
file "$binary"; \
! readelf -l "$binary" | grep -q INTERP || exit 1; \
! readelf -d "$binary" | grep -q NEEDED || exit 1; \
done
# Export binaries and their license files with --target artifacts --output.
FROM scratch AS artifacts
COPY --from=build /out/ /
# No libc, dynamic loader, package manager or shell in the runtime image.
FROM scratch AS runtime
COPY --from=build /out/ /usr/local/
ENTRYPOINT ["/usr/local/bin/ss-server"]
+8
View File
@@ -0,0 +1,8 @@
.git
build*
release
libcork
libipset
libbloom
**/__pycache__
**/.DS_Store
+41
View File
@@ -0,0 +1,41 @@
# Fully static Linux builds
The Alpine/musl builder uses Clang with LLD by default and links both third-party dependencies and libc statically.
`WITH_STATIC=ON` alone only selects static third-party libraries; this image also
passes `-DCMAKE_EXE_LINKER_FLAGS=-static` and omits shared-library outputs.
The compile, unit/vendor tests and six-method TCP/UDP relay tests run with Docker
network access disabled. The final ELF check rejects interpreters and dynamic
library dependencies. Initial image/toolchain installation requires networking.
From the repository root, export binaries and licenses (ARM64 example):
```sh
docker build --platform linux/arm64 -f docker/static/Dockerfile \
--target artifacts --output type=local,dest=build-artifacts/static-linux-arm64 .
```
Requires Docker Buildx/BuildKit. If Docker Hub is unavailable, add
`--build-arg ALPINE_IMAGE=public.ecr.aws/docker/library/alpine:3.24`.
An alternate package mirror can be selected with `--build-arg ALPINE_MIRROR=https://mirrors.aliyun.com/alpine`; APK signature checks remain enabled.
Docker build proxy settings apply to package installation; a broken configured
proxy can be bypassed for this command with empty `HTTP_PROXY`, `HTTPS_PROXY`,
`ALL_PROXY` build arguments and their lowercase equivalents.
Use `--platform linux/amd64` and a different destination for x86-64. Docker needs
a matching native builder or CPU emulation to compile and run the tests for a
foreign architecture. Add `--build-arg SS_MINIMAL=ON` for the minimal profile.
Build the scratch runtime image using the same cached build stage:
```sh
docker build --platform linux/arm64 -f docker/static/Dockerfile \
--target runtime -t shadowsocks-libev:static-arm64 .
docker run --rm -p 8388:8388/tcp -p 8388:8388/udp \
shadowsocks-libev:static-arm64 \
-s 0.0.0.0 -p 8388 -k example-password -m aes-256-gcm -u
```
The runtime image has no shell or external plugin executables. Supply any SIP003
plugin and its own runtime requirements separately. Configuration files can be
bind-mounted and passed with `-c`. Exported binaries run on Linux with the same
CPU architecture; they do not run directly on macOS or Windows.
+16
View File
@@ -147,3 +147,19 @@ submodule dependencies and reduces what is built without replacing audited
cryptographic algorithms with project-specific implementations.
See [measurements and tradeoffs](performance.md) for reproducible local results.
## Static build CI
The `static-macos` and `static-windows` jobs use Clang, bundled dependencies and
`SS_BUILD_SHARED_LIBRARY=OFF`. They run unit/vendor tests, relocate the installed
static library, build its consumer, and exercise the installed TCP/UDP programs.
The macOS job also exercises SIP003. `tests/check_static_runtime.py` rejects
non-system dylibs and DLLs (including Windows delay imports) in every installed
program and the static consumer. Windows runtime tests exclude toolchain DLL
paths. Windows uses the native [MSYS2 CLANG64 environment](https://www.msys2.org/docs/environments/).
macOS and Windows retain their OS libraries; static builds on these platforms
mean no separately distributed project, crypto, event-loop or toolchain shared
libraries. The `static-linux` job builds the Clang/musl Dockerfile, which also
links libc statically and rejects ELF interpreters and `NEEDED` entries. All
three jobs publish the tested installations as workflow artifacts.
+53
View File
@@ -0,0 +1,53 @@
#!/usr/bin/env python3
"""Reject non-system runtime dependencies in static macOS/Windows outputs."""
import argparse
import platform
import re
import subprocess
from pathlib import Path
WINDOWS_SYSTEM_DLLS = {
'advapi32.dll', 'bcrypt.dll', 'crypt32.dll', 'dnsapi.dll', 'iphlpapi.dll',
'kernel32.dll', 'msvcrt.dll', 'ntdll.dll', 'ole32.dll', 'psapi.dll',
'rpcrt4.dll', 'secur32.dll', 'shell32.dll', 'ucrtbase.dll', 'user32.dll',
'userenv.dll', 'winmm.dll', 'ws2_32.dll',
}
def dependencies(binary, system):
if system == 'Darwin':
output = subprocess.check_output(['otool', '-L', str(binary)], text=True)
return [line.strip().split(' (compatibility version', 1)[0]
for line in output.splitlines() if line.startswith('\t')]
output = subprocess.check_output(
['llvm-readobj', '--coff-imports', str(binary)], text=True)
return re.findall(r'^\s+Name: (.+)$', output, re.MULTILINE)
def is_system_dependency(name, system):
if system == 'Darwin':
return name.startswith(('/usr/lib/', '/System/Library/'))
name = name.strip().lower()
return name in WINDOWS_SYSTEM_DLLS or (
name.startswith(('api-ms-win-', 'ext-ms-win-')) and name.endswith('.dll'))
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('binaries', type=Path, nargs='+')
args = parser.parse_args()
system = platform.system()
if system not in ('Darwin', 'Windows'):
parser.error('this check supports macOS and native Windows Python')
for binary in args.binaries:
names = dependencies(binary, system)
if not names:
raise SystemExit(f'{binary}: no OS imports found; check file format/tool output')
unexpected = [name for name in names if not is_system_dependency(name, system)]
if unexpected:
raise SystemExit(f'{binary}: non-system dependencies: {unexpected}')
print(f'PASS {binary}: {", ".join(names)}')
if __name__ == '__main__':
main()