Skip to content

Instantly share code, notes, and snippets.

@tanbro
Last active August 3, 2022 07:13
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save tanbro/55011baac54db8456556493abb9e4092 to your computer and use it in GitHub Desktop.
Save tanbro/55011baac54db8456556493abb9e4092 to your computer and use it in GitHub Desktop.
使用 quay.io 提供的 pypa/manylinux Docker 镜像构建该项目及其依赖软件的 Python Wheel 发布包
#!/usr/bin/env python3
"""
使用 quay.io 提供的 pypa/manylinux Docker 镜像构建该项目及其依赖软件的 Python Wheel 发布包
"""
import argparse
import os
import os.path
import platform
import shlex
import sys
from itertools import chain
from locale import getpreferredencoding
from subprocess import check_call, run
from textwrap import dedent, indent
_IMAGE_LIST = [
# manylinux_2_28 (AlmaLinux 8 based)
'manylinux_2_28_x86_64',
'manylinux_2_28_aarch64',
'manylinux_2_28_ppc64le',
# manylinux_2_24 (Debian 9 based)
'manylinux_2_24_x86_64',
'manylinux_2_24_i686',
'manylinux_2_24_aarch64',
'manylinux_2_24_ppc64le',
'manylinux_2_24_s390x',
# manylinux2014 (CentOS 7 based)
'manylinux2014_x86_64',
'manylinux2014_i686',
'manylinux2014_aarch64',
'manylinux2014_ppc64le',
'manylinux2014_s390x',
# manylinux2010 (CentOS 6 based - EOL)
'manylinux2010_x86_64',
'manylinux2010_i686',
# manylinux1 (CentOS 5 based - EOL)
'manylinux1_x86_64',
'manylinux1_i686',
]
_PYTHON_LIST = [
'36',
'37',
'38',
'39',
'310',
'311',
]
def get_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
'--python', '-P', type=str, choices=_PYTHON_LIST, default='36',
help='要使用的 Docker 镜像中的 Python 版本. "36" 代表 "python 3.6", "310" 代表 "python 3.10" ... '
'(default=%(default)s).'
)
parser.add_argument(
'--implementation', '-m', type=str, choices={'cpython', 'pypy'}, default='cpython',
help='要使用的 C Python 还是 PyPy (default=%(default)s).'
)
parser.add_argument(
'--image', '-i', type=str, choices=_IMAGE_LIST, default=f'manylinux2014_{platform.machine()}',
help='使用这个 pypa/manylinux Docker 镜像进行构建. (default=%(default)s). '
'参见 https://github.com/pypa/manylinux'
)
parser.add_argument(
'--outdir', '-o', type=str,
help=f'输出目录 (default="wheels/{{impl}}{{python}}-{{image}}")'
)
parser.add_argument(
'--paths', '-a', type=str, nargs='+', action='append',
help='要安装的 VCS 或本地项目源代码路径。可多次指定。'
)
parser.add_argument(
'--requires', '-r', type=str, nargs='+', action='append',
help='从这些指定的 PyPI requirements 文件安装软件包。可多次指定。'
)
parser.add_argument(
'--packages', '-p', type=str, nargs='+', action='append',
help='要安装的 PyPI 软件包。可多次指定。'
)
if hasattr(argparse, 'BooleanOptionalAction'):
parser.add_argument(
'--root', action=argparse.BooleanOptionalAction,
help=f'是否以 root 身份在 Docker 中执行. (default=%(default)s). '
)
else:
parser.add_argument(
'--root', action='store_true',
help=f'要以 root 身份在 Docker 中执行. (default=%(default)s). '
)
return parser.parse_args()
_INSTALL_ARGS = 'paths', 'requires', 'packages'
def main(args: argparse.Namespace):
python_version = f'{args.python[0]}.{args.python[1:]}'
if args.implementation == 'cpython':
python_exe = f'python{python_version}'
dir_prefix = 'cp'
elif args.implementation == 'pypy':
python_exe = f'pypy{python_version}'
dir_prefix = 'pp'
else:
raise ValueError(f'Unknown implementation "{args.implementation}"')
if not args.outdir:
args.outdir = os.path.join('wheels', f'{dir_prefix}{args.python}-{args.image}')
# flatten
for name in _INSTALL_ARGS:
value = getattr(args, name)
if value is not None:
setattr(args, name, list(chain.from_iterable(value)))
if not any(getattr(args, name) for name in _INSTALL_ARGS):
print('参数 {} 必须至少其中指定一项'.format(_INSTALL_ARGS), file=sys.stderr)
return 1
# docker 命令
run_args = ['docker', 'run', '--rm']
# user mount
if not args.root and all(hasattr(os, name) for name in ('getuid', 'getgid')):
run_args.extend(['-u', f'{os.getuid()}:{os.getgid()}'])
# dir mount
workdir = '/var/workspace'
run_args.extend(['-w', f'{workdir}'])
run_args.extend(['-v', f'{os.getcwd()}:{workdir}'])
# cache dir mount
pip_cache_dir = ''
if not args.root:
cpp = run(
'pip cache dir',
shell=True, capture_output=True, encoding=getpreferredencoding()
)
if cpp.returncode == 0:
host_pip_cache_dir = cpp.stdout.strip()
if host_pip_cache_dir:
pip_cache_dir = '/var/pip-cache'
run_args.extend(
['-v', f'{host_pip_cache_dir}:{pip_cache_dir}']
)
else:
for s in (cpp.stdout, cpp.stderr):
if s and not s.isspace():
print(s, file=sys.stderr)
# wheels output dir
whl_dir = os.path.join(workdir, args.outdir)
# docker image name
image_name = f'quay.io/pypa/{args.image}'
run_args.append(image_name)
# sh 命令
run_args.extend(['/bin/sh', '-c'])
# 用这个字符串拼凑 bash -c 要执行的命令:
bash_script = 'set -e'
# 首先处理 git
# bash_script += dedent(f'''
# git config --global --add safe.directory {workdir}
# ''')
# 组合 pip wheel 命令
pip_args = [python_exe, '-m', 'pip', 'wheel', '-w', whl_dir]
# pip_cache_dir
if not args.root:
if pip_cache_dir:
pip_args.extend(['--cache-dir', pip_cache_dir])
else:
pip_args.append('--no-cache-dir')
# VCS/本项目的 pip 安装指示符
if args.paths:
for spec in args.paths:
pip_args.extend(['-e', spec])
if args.requires: # 命令行参数中附加的 requirements 文件
for require in args.requires: # --requirement
pip_args.extend(['-r', require])
if args.packages: # 命令行参数中单独指定要安装的
for package in args.packages:
pip_args.append(package)
# 特殊的环境变量 PYPI_INDEX_URL,从本地读取,在 container 的命令行中使用
pypi_index_url = os.getenv('PYPI_INDEX_URL', '').strip()
if pypi_index_url:
pip_args.extend(['-i', pypi_index_url])
# 加入 pip 命令到 bash 脚本
bash_script += dedent(f'''
{shlex.join(pip_args)}
''')
# 去掉空行, 转换换行符
bash_script = '\n'.join(
filter(None, (
line.strip() for line in bash_script.splitlines()
))
).strip()
# 打印 bash 命令行
print(
f'Run bash script in docker image "{image_name}" for "{python_exe}":')
print(indent(bash_script, '\t> '))
# 加上 bash 命令
run_args.append(bash_script)
# mkdir
print(f'Output dir: {args.outdir}')
os.makedirs(args.outdir, exist_ok=True)
# docker run ...
check_call(run_args)
if __name__ == '__main__':
exit(main(get_args()))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment