Files
VPSMaster/install.py
T

110 lines
5.5 KiB
Python

#!/usr/bin/env python3
"""Offline, dependency-free installer. Review this file before running it."""
import argparse
import hashlib
import json
import os
from pathlib import Path, PurePosixPath
import shutil
import subprocess
import sys
VERSION = '1.0.0b1'
def verify(source):
manifest = json.loads((source/'MANIFEST.sha256.json').read_text(encoding='utf-8'))
if manifest.get('version') != VERSION or not isinstance(manifest.get('files'), dict):
raise ValueError('Invalid release manifest')
files = manifest['files']
required = {'install.py','pyproject.toml','LICENSE','THIRD_PARTY_NOTICES.md',
'proxydoctor/__init__.py','proxydoctor/__main__.py','proxydoctor/cli.py'}
if not required <= set(files):
raise ValueError('Incomplete release manifest')
actual = {p.relative_to(source).as_posix() for p in (source/'proxydoctor').rglob('*.py')}
if actual != {name for name in files if name.startswith('proxydoctor/') and name.endswith('.py')}:
raise ValueError('Unlisted or missing package source')
for name, expected in files.items():
relative = PurePosixPath(name)
if relative.is_absolute() or '..' in relative.parts or '\\' in name:
raise ValueError('Unsafe manifest path')
path = source.joinpath(*relative.parts)
if any(part.is_symlink() for part in (path,*path.parents) if part != source.parent):
raise ValueError('Symlink in source path')
if not path.is_file() or hashlib.sha256(path.read_bytes()).hexdigest() != expected:
raise ValueError('Release integrity check failed')
return files
def install(source, prefix):
if sys.version_info < (3,10) or sys.platform != 'linux':
raise ValueError('Requires Linux and Python 3.10+')
files = verify(source)
prefix = prefix.expanduser().absolute()
if prefix.is_symlink() or os.path.lexists(prefix):
raise FileExistsError('Install destination already exists; choose a new version directory')
prefix.parent.mkdir(parents=True,exist_ok=True)
prefix.mkdir(mode=0o700)
owned = prefix.resolve()
try:
# --without-pip avoids ensurepip/network/package-manager dependencies.
subprocess.run([sys.executable,'-I','-B','-m','venv','--without-pip',str(prefix)],
check=True,timeout=45,capture_output=True)
python = prefix/'bin/python'
probe = subprocess.run([str(python),'-I','-B','-c',
'import json,sys,sysconfig;print(json.dumps([sys.prefix,sys.base_prefix,sysconfig.get_path("purelib")]))'],
check=True,capture_output=True,text=True,timeout=10)
active,base,library = json.loads(probe.stdout)
if active == base or Path(active).resolve() != owned:
raise ValueError('Virtual environment isolation check failed')
library = Path(library)
library.resolve().relative_to(owned)
for name in files:
if name.startswith('proxydoctor/') and name.endswith('.py'):
destination = library/name
destination.parent.mkdir(parents=True,exist_ok=True)
shutil.copyfile(source/name,destination)
notices = prefix/'share/proxydoctor'
notices.mkdir(parents=True)
for name in ('LICENSE','THIRD_PARTY_NOTICES.md','MANIFEST.sha256.json'):
shutil.copyfile(source/name,notices/name)
launcher = prefix/'bin/vpsmaster'
# execv avoids shebang/path whitespace issues without shell interpolation.
launcher.write_text('#!/bin/sh\nexec '+__import__('shlex').quote(str(python))+' -I -B -m proxydoctor "$@"\n',encoding='utf-8')
launcher.chmod(0o700)
legacy_launcher = prefix/'bin/proxydoctor'
legacy_launcher.write_bytes(launcher.read_bytes())
legacy_launcher.chmod(0o700)
subprocess.run([str(launcher),'--version'],check=True,timeout=10,capture_output=True)
(prefix/'proxydoctor-install.json').write_text(json.dumps({'brand':'VPSMaster','package':'proxydoctor','version':VERSION,'isolated':True}),encoding='utf-8')
return launcher
except BaseException:
# Only remove the new directory reserved by this invocation.
if not prefix.is_symlink() and prefix.resolve() == owned:
shutil.rmtree(prefix)
raise
def main():
parser=argparse.ArgumentParser(description='Offline isolated VPSMaster installer / VPSMaster 离线隔离安装')
parser.add_argument('--prefix',type=Path,default=Path.home()/'.local/share/vpsmaster'/VERSION)
parser.add_argument('--run',action='store_true',help='Install and run a read-only offline check')
args=parser.parse_args()
try:
launcher=install(Path(__file__).resolve().parent,args.prefix)
except KeyboardInterrupt:
print('Installation cancelled; new environment cleaned / 安装已取消',file=sys.stderr)
return 130
except (OSError,ValueError,subprocess.SubprocessError):
print('Installation failed. Check Python/venv availability, release checksum and destination. No system packages were installed. / 安装失败,请检查环境、校验和与目录。',file=sys.stderr)
return 2
print('Installed / 已安装:'+str(launcher),flush=True)
print('VPSMaster 1.0 public beta; not the final release / VPSMaster 1.0 公测版,非最终版本',flush=True)
if args.run:
return subprocess.call([str(launcher),'suite','--offline'])
return 0
if __name__=='__main__':
sys.exit(main())