143 lines
8.2 KiB
Python
143 lines
8.2 KiB
Python
#!/usr/bin/env python3
|
|
"""Fetch pinned companions, verify hashes and build static Linux wg tools."""
|
|
import concurrent.futures
|
|
import hashlib
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import shutil
|
|
import subprocess
|
|
import tarfile
|
|
import urllib.request
|
|
|
|
ROOT=Path(__file__).resolve().parent.parent
|
|
CACHE=ROOT/'cache'; CACHE.mkdir(exist_ok=True)
|
|
(CACHE/'go.mod').write_text('module wswg-build-cache\n\ngo 1.24\n')
|
|
LOCK=ROOT/'scripts/dependencies.lock.json'
|
|
known=json.loads(LOCK.read_text()) if LOCK.exists() else {}
|
|
update=os.environ.get('UPDATE_LOCK')=='1'
|
|
WST='11.1.0'; WG='1.0.20250521'; WIN='1.1.1'; MUSL='1.2.3'
|
|
|
|
def fetch(url):
|
|
entry=known.get(url)
|
|
download_url=entry.get('url',url) if isinstance(entry,dict) else url
|
|
path=CACHE/url.rsplit('/',1)[-1]
|
|
if not path.exists():
|
|
print('Download',path.name,flush=True)
|
|
tmp=path.with_suffix(path.suffix+'.part')
|
|
with urllib.request.urlopen(download_url,timeout=90) as r,tmp.open('wb') as f: shutil.copyfileobj(r,f)
|
|
tmp.replace(path)
|
|
digest=hashlib.file_digest(path.open('rb'),'sha256').hexdigest()
|
|
if url not in known:
|
|
if not update: raise RuntimeError(f'Unpinned dependency: {url}. Use UPDATE_LOCK=1 only when reviewing a dependency update.')
|
|
known[url]=digest
|
|
if (known[url].get('sha256') if isinstance(known[url],dict) else known[url])!=digest: raise RuntimeError(f'Checksum mismatch: {path}')
|
|
return path
|
|
|
|
def extract(archive,dest):
|
|
dest.mkdir(parents=True,exist_ok=True)
|
|
with tarfile.open(archive) as t:
|
|
root=dest.resolve()
|
|
for m in t.getmembers():
|
|
target=(root/m.name).resolve()
|
|
if not target.is_relative_to(root) or not (m.isfile() or m.isdir() or m.issym() or m.islnk()):
|
|
raise RuntimeError(f'Unsafe archive member: {m.name}')
|
|
if m.issym() or m.islnk():
|
|
link=((target.parent if m.issym() else root)/m.linkname).resolve()
|
|
if not link.is_relative_to(root): raise RuntimeError(f'Unsafe archive link: {m.name}')
|
|
t.extractall(dest)
|
|
|
|
def run(args,**kw):
|
|
logfile=CACHE/'build.log'
|
|
with logfile.open('a') as log:
|
|
r=subprocess.run(args,stdout=log,stderr=subprocess.STDOUT,**kw)
|
|
if r.returncode: raise RuntimeError(f'{args} failed. See {logfile}')
|
|
|
|
def check_static(path):
|
|
for args,marker in [(['readelf','-l',str(path)],'INTERP'),(['readelf','-d',str(path)],'(NEEDED)')]:
|
|
if marker in subprocess.check_output(args,text=True): raise RuntimeError(f'Dynamic dependency in {path}')
|
|
|
|
def copy(src,dst):
|
|
dst.parent.mkdir(parents=True,exist_ok=True);shutil.copy2(src,dst)
|
|
|
|
def static_wg(arch,archive,musl):
|
|
result=CACHE/f'wg-static-{arch}'
|
|
if result.exists(): return result
|
|
compilers={'amd64':('gcc','x86_64',[]),'386':('gcc','i386',['-m32']), 'arm':('arm-linux-gnueabi-gcc','arm',['-march=armv6','-mfloat-abi=soft']), 'armv7':('arm-linux-gnueabihf-gcc','arm',['-march=armv7-a','-mfpu=vfpv3-d16','-mfloat-abi=hard']), 'arm64':('aarch64-linux-gnu-gcc','aarch64',[])}
|
|
cc,host,flags=compilers[arch];work=CACHE/f'compile-{arch}';extract(musl,work)
|
|
source=work/f'musl-{MUSL}';prefix=work/'musl-install'
|
|
prefixcc=cc.removesuffix('gcc')
|
|
env=dict(os.environ,CC=cc,REALGCC=cc,AR=prefixcc+'ar',RANLIB=prefixcc+'ranlib',CFLAGS='-Os '+' '.join(flags))
|
|
if flags: env['CC']=cc+' '+' '.join(flags)
|
|
run(['./configure',f'--prefix={prefix}',f'--target={host}','--disable-shared'],cwd=source,env=env)
|
|
run(['make','-j4'],cwd=source,env=env);run(['make','install'],cwd=source,env=env)
|
|
extract(archive,work);wg=work/f'wireguard-tools-{WG}'/'src'
|
|
# Add only kernel UAPI headers, never the host glibc headers.
|
|
headers=work/'kernel-headers';headers.mkdir(exist_ok=True)
|
|
kernelbase=Path('/usr/include') if cc=='gcc' else Path('/usr')/cc.removesuffix('-gcc')/'include'
|
|
for name in ['linux','asm-generic']:
|
|
shutil.copytree(kernelbase/name,headers/name,dirs_exist_ok=True)
|
|
asm=Path('/usr/include/x86_64-linux-gnu/asm') if cc=='gcc' else kernelbase/'asm'
|
|
shutil.copytree(asm,headers/'asm',dirs_exist_ok=True)
|
|
(headers/'linux/wireguard.h').unlink(missing_ok=True)
|
|
run(['make','-j4',f'CPPFLAGS=-I{headers}', f'CC={prefix}/bin/musl-gcc {" ".join(flags)}','LDFLAGS=-static'+(' -Wl,-m,elf_i386' if arch=='386' else ''),'wg'],cwd=wg,env=env)
|
|
copy(wg/'wg',result);result.chmod(0o755)
|
|
return result
|
|
|
|
def main():
|
|
wg=fetch(f'https://git.zx2c4.com/wireguard-tools/snapshot/wireguard-tools-{WG}.tar.xz')
|
|
musl=fetch(f'https://deb.debian.org/debian/pool/main/m/musl/musl_{MUSL}.orig.tar.gz')
|
|
extract(wg,CACHE/'wg-source');wgsrc=CACHE/'wg-source'/f'wireguard-tools-{WG}'
|
|
base=f'https://github.com/erebe/wstunnel/releases/download/v{WST}/'
|
|
sums=fetch(base+'checksums.txt');expected={}
|
|
for line in sums.read_text().splitlines():
|
|
parts=line.split()
|
|
if len(parts)==2: expected[parts[1].lstrip('*')]=parts[0]
|
|
targets=sorted(p for p in (ROOT/'dist').glob('wswg-*') if p.is_dir())
|
|
urls=[]
|
|
for folder in targets:
|
|
platform,arch=folder.name.removeprefix('wswg-').split('-',1)
|
|
osname='linux' if platform=='termux' else platform
|
|
wa='armv6' if arch=='arm' else arch
|
|
urls.append(base+f'wstunnel_{WST}_{osname}_{wa}.tar.gz')
|
|
if platform=='windows':
|
|
winarch={'386':'x86','amd64':'amd64','arm64':'arm64'}[arch]
|
|
urls.extend([f'https://download.wireguard.com/windows-client/wireguard-{winarch}-{WIN}.msi',f'https://aka.ms/vs/17/release/vc_redist.{"x64" if arch=="amd64" else winarch}.exe'])
|
|
# Independent downloads only; all archive extraction and builds stay sequential.
|
|
with concurrent.futures.ThreadPoolExecutor(max_workers=4) as pool:
|
|
files=dict(zip(sorted(set(urls)),pool.map(fetch,sorted(set(urls)))))
|
|
for folder in targets:
|
|
platform,arch=folder.name.removeprefix('wswg-').split('-',1);osname='linux' if platform=='termux' else platform;wa='armv6' if arch=='arm' else arch
|
|
archive=files[base+f'wstunnel_{WST}_{osname}_{wa}.tar.gz']
|
|
actual=hashlib.file_digest(archive.open('rb'),'sha256').hexdigest()
|
|
if expected.get(archive.name)!=actual: raise RuntimeError(f'Upstream wstunnel checksum mismatch: {archive.name}')
|
|
unpack=CACHE/(archive.name+'.unpacked');extract(archive,unpack)
|
|
name='wstunnel.exe' if platform=='windows' else 'wstunnel'
|
|
binary=next(unpack.rglob(name));copy(binary,folder/'bin'/name);(folder/'bin'/name).chmod(0o755)
|
|
licenses=folder/'licenses';licenses.mkdir(exist_ok=True)
|
|
copy(wgsrc/'COPYING',licenses/'wireguard-tools.txt')
|
|
wlicense=fetch(f'https://raw.githubusercontent.com/erebe/wstunnel/v{WST}/LICENSE')
|
|
copy(wlicense,licenses/'wstunnel.txt')
|
|
if platform=='windows':
|
|
winarch={'386':'x86','amd64':'amd64','arm64':'arm64'}[arch]
|
|
msi=files[f'https://download.wireguard.com/windows-client/wireguard-{winarch}-{WIN}.msi']
|
|
unpack=CACHE/(msi.name+'.unpacked');unpack.mkdir(exist_ok=True)
|
|
run(['msiextract','-C',str(unpack),str(msi)])
|
|
for name in ['wireguard.exe','wg.exe']: copy(next(unpack.rglob(name)),folder/'bin'/name)
|
|
redarch='x64' if arch=='amd64' else winarch
|
|
redist=files[f'https://aka.ms/vs/17/release/vc_redist.{redarch}.exe'];copy(redist,folder/'redist'/redist.name)
|
|
wlicense=fetch('https://raw.githubusercontent.com/WireGuard/wireguard-windows/v1.1.1/COPYING');copy(wlicense,licenses/'wireguard-windows.txt')
|
|
else:
|
|
check_static(binary)
|
|
static=static_wg(arch,wg,musl);check_static(static)
|
|
copy(static,folder/'bin'/'wg')
|
|
quick=wgsrc/'src/wg-quick/linux.bash';copy(quick,folder/'bin'/'wg-quick');(folder/'bin'/'wg-quick').chmod(0o755)
|
|
if platform=='termux':
|
|
# A static Linux ELF runs under Android's kernel. No Bionic shared libraries.
|
|
# Use Termux's bash: Android does not have /bin/bash.
|
|
q=folder/'bin'/'wg-quick';q.write_text(q.read_text().replace('#!/bin/bash','#!/data/data/com.termux/files/usr/bin/bash',1))
|
|
copy(CACHE/f'compile-{arch}'/f'musl-{MUSL}'/'COPYRIGHT',licenses/'musl.txt')
|
|
if update: LOCK.write_text(json.dumps(known,indent=2,sort_keys=True)+'\n')
|
|
print('Companions bundled.',flush=True)
|
|
|
|
if __name__=='__main__': main()
|