Ubuntu+Python镜像构造脚本
这个 Python 脚本是一个交互式工具,用于生成 Dockerfile 和 Makefile,以便快速构建一个定制化的 Python 开发环境的 Docker 镜像。它会引导用户选择基础 Ubuntu 版本、Python 版本、包管理器、软件源以及其他额外的软件包。
执行以下命令:
shell
wget https://alman.netlify.app/code/dup.py
# default
python3 dup.py --ubuntu 24 --ubuntu-mirror 3 --python 12 --pip-mirror 2 --pm 2 --extra-python-pkg 1 --extra-packages make --force
# 生成一个基于 Ubuntu 22.04,使用阿里云源,安装 Python 3.10,使用清华 pip 源和 uv 包管理工具的 Dockerfile
python3 dup.py --ubuntu 22 --ubuntu-mirror 3 --python 10 --pip-mirror 2 --pm 2
# 生成一个基于 Ubuntu 20.04,安装 Python 开发依赖,安装 make 和 curl 的 Dockerfile,并强制覆盖现有文件
python3 dup.py --ubuntu 20 --python 2 --extra-packages make curl --force
# selenium
python3 dup.py --ubuntu 22 --ubuntu-mirror 3 --python 11 --pip-mirror 2 --pm 2 --extra-python-pkg 2 --extra-packages make --force脚本代码
python
#!/usr/bin/env python3
import argparse
import os
import sys
class DockerfileGenerator:
# Color constants
YELLOW = '\033[1;33m'
CYAN = '\033[1;36m'
BLUE = '\033[1;34m'
RED = '\033[1;31m'
NC = '\033[0m'
def __init__(self, args=None):
self.args = args
self.ubuntu_choice = None
self.ubuntu_mirror_choice = None
self.pip_mirror = None
self.pip_mirror_choice = None # Store the choice number
self.python_choice = None
self.py_pm_choice = None
self.py_pm_choice_key = None # Store the choice key
self.extra_python_pkg_choice = None
self.selected_extra_packages = []
def _select_from_menu(self, options_map, order_list, prompt, invalid_msg, arg_value=None):
"""
选择菜单选项,优先使用命令行参数值,否则进入交互模式。
"""
if arg_value is not None and str(arg_value) in options_map:
choice = str(arg_value)
print(f"{self.BLUE}使用命令行参数选择: {options_map[choice]}{self.NC}\n")
return choice
while True:
print(f"{self.YELLOW}{prompt}{self.NC}")
for idx in order_list:
print(f"{self.CYAN}{idx}) {options_map[idx]}{self.NC}")
choice = input(f"请输入你的选择({' '.join(map(str, order_list))}): ")
if choice in options_map:
print(f"{self.BLUE}你选择了{options_map[choice]}{self.NC}\n")
return choice
print(f"{self.RED}{invalid_msg}{self.NC}")
def _write_to_dockerfile(self, content):
"""将内容写入 Dockerfile 文件"""
with open("Dockerfile", "a") as f:
f.write(f"{content}\n")
def select_ubuntu_version(self):
ubuntu_map = {
"20": "ubuntu:20.04",
"22": "ubuntu:22.04",
"24": "ubuntu:24.04"
}
ubuntu_label_map = {
"20": "Ubuntu 20.04 LTS(Focal Fossa)",
"22": "Ubuntu 22.04 LTS(Jammy Jellyfish)",
"24": "Ubuntu 24.04 LTS(Noble Numbat)"
}
choice = self._select_from_menu(ubuntu_label_map, list(ubuntu_map.keys()), "选择Ubuntu版本:", "无效的选择,请重新选择。", arg_value=self.args.ubuntu if self.args else None)
with open("Dockerfile", "w") as f:
f.write(f"FROM {ubuntu_map[choice]}\n")
self.ubuntu_choice = choice
def select_ubuntu_mirror(self):
mirror_map = {
"1": "官方源(不修改)",
"2": "mirrors.ustc.edu.cn",
"3": "mirrors.aliyun.com"
}
choice = self._select_from_menu(mirror_map, ["1", "2", "3"], "选择Ubuntu的源:", "无效的选择,请重新选择。", arg_value=self.args.ubuntu_mirror if self.args else None)
if choice != "1":
self._write_to_dockerfile("\n#设置mirror源")
mirror_url = mirror_map[choice].split("(")[0] # Extract URL part
self._write_to_dockerfile(f"RUN sed -i 's/archive.ubuntu.com/{mirror_url}/g' /etc/apt/sources.list")
self._write_to_dockerfile("RUN apt update")
self.ubuntu_mirror_choice = choice
def set_default_timezone(self):
self._write_to_dockerfile("\n#设置时区")
self._write_to_dockerfile("RUN DEBIAN_FRONTEND=noninteractive TZ=Asia/Shanghai apt-get -y install tzdata")
self._write_to_dockerfile("ENV TZ=Asia/Shanghai")
print(f"{self.BLUE}设置时区为: Asia/Shanghai{self.NC}")
def select_python_version(self):
python_versions = {
"1": "不安装",
"2": "安装开发依赖",
"7": "Python3.7",
"8": "Python3.8",
"9": "Python3.9",
"10": "Python3.10",
"11": "Python3.11",
"12": "Python3.12",
"13": "Python3.13",
}
ubuntu_python_map = {
"20": "Python3.8",
"22": "Python3.10",
"24": "Python3.12",
}
# Mark system-自带 versions
if self.ubuntu_choice in ubuntu_python_map.keys():
v = ubuntu_python_map[self.ubuntu_choice]
for ver in python_versions.keys():
if python_versions[ver] == v and "(系统自带)" not in python_versions[ver]:
python_versions[ver] = f"{v} (系统自带)"
choice = self._select_from_menu(python_versions, list(python_versions.keys()), "选择Python的版本:", "无效的选择,请重新选择。", arg_value=self.args.python if self.args else None)
self._write_to_dockerfile("\n#安装Python")
if choice == "1":
pass
elif choice == "2":
self._write_to_dockerfile("RUN apt install -y python3 python3-dev python3-pip")
else:
version = choice
if version == "11" and self.ubuntu_choice == "24":
self._write_to_dockerfile("RUN apt install -y software-properties-common")
self._write_to_dockerfile("RUN add-apt-repository ppa:deadsnakes/ppa")
self._write_to_dockerfile("RUN apt update")
self._write_to_dockerfile("RUN apt install -y python3.11-distutils")
self._write_to_dockerfile(f"RUN apt install -y python3.{version} python3.{version}-dev python3-pip")
self._write_to_dockerfile(f"RUN ln -sf /usr/bin/python3.{version} /usr/bin/python3")
self.python_choice = choice
def select_python_pip_mirror(self):
mirror_map = {
"1": "不使用",
"2": "https://pypi.tuna.tsinghua.edu.cn/simple",
"3": "https://mirrors.sustech.edu.cn/pypi/web/simple",
}
choice = self._select_from_menu(mirror_map, list(mirror_map.keys()), "选择pip源:", "无效的选择,请重新选择。", arg_value=self.args.pip_mirror if self.args else None)
self.pip_mirror_choice = choice
self.pip_mirror = "" if choice == "1" else mirror_map[choice]
return self.pip_mirror
def select_python_pm(self):
pm_map = {
"1": "pip",
"2": "uv (推荐)",
"3": "poetry",
"4": "pdm",
}
choice = self._select_from_menu(pm_map, list(pm_map.keys()), "选择Python包管理工具:", "无效的选择,请重新选择。", arg_value=self.args.pm if self.args else None)
self.py_pm_choice_key = choice
self.py_pm_choice = pm_map[choice].replace(' (推荐)','')
if choice != "1":
self._write_to_dockerfile(f"\n#安装{self.py_pm_choice}")
mirror_arg = f" -i {self.pip_mirror}" if self.pip_mirror else ""
# self._write_to_dockerfile(f"RUN python3 -m pip install {self.py_pm_choice} --break-system-packages{mirror_arg}")
self._write_to_dockerfile(f"RUN python3 -m pip install {self.py_pm_choice} {mirror_arg}")
def select_python_extra(self):
dockerfile_command_selenium = """# Install curl and other dependencies required for later steps
RUN apt-get install -y \
curl \
wget \
unzip \
libglib2.0-0 \
libnss3 \
libgconf-2-4 \
libfontconfig1 \
fonts-wqy-zenhei
# Add Google Chrome repository and install Chromium
RUN curl -LO https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb && \
apt-get install -y ./google-chrome-stable_current_amd64.deb && \
rm google-chrome-stable_current_amd64.deb && \
apt-get clean
RUN export CHROME_VERSION=$(google-chrome --version | awk '{print $3}') && \
echo "Chrome version installed: $CHROME_VERSION" && \
wget -q https://storage.googleapis.com/chrome-for-testing-public/$CHROME_VERSION/linux64/chromedriver-linux64.zip -O /tmp/chromedriver.zip
RUN unzip /tmp/chromedriver.zip -d /usr/local/bin/
RUN ls -al /usr/local/bin/
RUN rm /tmp/chromedriver.zip && chmod +x /usr/local/bin/chromedriver-linux64/chromedriver"""
dockerfile_command_dotnet = """RUN apt install -y make
RUN apt-get install -y --no-install-recommends ca-certificates curl software-properties-common
RUN curl -sSL https://packages.microsoft.com/keys/microsoft.asc | apt-key add -
RUN add-apt-repository "$(curl -sSL https://packages.microsoft.com/config/ubuntu/20.04/prod.list)"
RUN apt-get update && apt-get install -y --no-install-recommends dotnet-sdk-6.0
ENV PYTHONNET_RUNTIME=dotnet"""
python_extra_pkg_map = {
"1": "不安装(推荐)",
"2": "安装selenium和chromedriver",
"3": "安装.NET SDK"
}
choice = self._select_from_menu(python_extra_pkg_map, list(python_extra_pkg_map.keys()), "选择额外安装的Python相关包:", "无效的选择,请重新选择。", arg_value=self.args.extra_python_pkg if self.args else None)
self.extra_python_pkg_choice = choice
choice_label = python_extra_pkg_map[choice]
if choice == '2':
self._write_to_dockerfile(f"\n#{choice_label}")
self._write_to_dockerfile(dockerfile_command_selenium)
print(f"{self.BLUE}{choice_label}{self.NC}")
elif choice == '3':
self._write_to_dockerfile(f"\n#{choice_label}")
# The original code always wrote dotnet command regardless of choice 2 or 3
# Let's assume it meant to write dotnet command ONLY for choice 3
self._write_to_dockerfile(dockerfile_command_dotnet)
print(f"{self.BLUE}{choice_label}{self.NC}")
def read_dockerfile(self):
print(f"{self.BLUE}Dockerfile已生成,内容如下:{self.NC}")
with open("Dockerfile", "r") as f:
print(f.read())
def install_extra_packages(self):
extra_packages_map = {
"1": "不安装",
"2": "make",
"3": "curl",
"4": "wget",
"5": "libpq-dev (for pgsql)"
}
extra_packages_keys = list(extra_packages_map.keys())
# Map name back to key for args lookup
extra_packages_names = {v.split(" ")[0]:k for k,v in extra_packages_map.items() if k != '1'}
# Handle arguments for extra packages
if self.args and self.args.extra_packages:
selected_by_arg = []
invalid_packages = []
for pkg_name in self.args.extra_packages:
if pkg_name in extra_packages_names:
selected_by_arg.append(extra_packages_map[extra_packages_names[pkg_name]].split(" ")[0])
else:
invalid_packages.append(pkg_name)
if invalid_packages:
print(f"{self.RED}无效的额外包名通过参数提供: {', '.join(invalid_packages)}. 请从以下选择: {', '.join([extra_packages_map[k].split(' ')[0] for k in extra_packages_keys if k != '1'])}{self.NC}")
self.selected_extra_packages = selected_by_arg
if self.selected_extra_packages:
print(f"{self.BLUE}使用命令行参数选择额外包: {', '.join(self.selected_extra_packages)}{self.NC}\n")
# If no arguments or arguments were insufficient, fall back to interactive
if not self.selected_extra_packages or (self.args and not self.args.extra_packages):
print(f"{self.YELLOW}选择额外安装的包 (选择1结束):{self.NC}")
for idx in extra_packages_keys:
print(f"{self.CYAN}{idx}) {extra_packages_map[idx]}{self.NC}")
while True:
choice = input(f"请输入你的选择({' '.join(extra_packages_keys)}): ")
if choice == "1":
break
if choice in extra_packages_map:
package_name = extra_packages_map[choice].split(" ")[0]
if package_name not in self.selected_extra_packages:
self.selected_extra_packages.append(package_name)
print(f"{self.BLUE}已选择的包: {', '.join(self.selected_extra_packages)}{self.NC}")
else:
print(f"{self.YELLOW}包 '{package_name}' 已选择。{self.NC}")
else:
print(f"{self.RED}无效的选择,请重新选择。{self.NC}")
if self.selected_extra_packages:
self._write_to_dockerfile("\n#安装额外的包")
self._write_to_dockerfile(f"RUN apt install -y {' '.join(self.selected_extra_packages)}")
def clear_apt_cache(self):
self._write_to_dockerfile("\n#清理apt缓存")
self._write_to_dockerfile("RUN apt clean")
self._write_to_dockerfile("RUN rm -rf /var/lib/apt/lists/*")
self._write_to_dockerfile("RUN rm -rf /var/cache/apt/archives/*")
print(f"{self.BLUE}已清理apt缓存{self.NC}")
@property
def image_tag(self):
"""根据选择生成镜像标签"""
tag_parts = [f'u{self.ubuntu_choice}']
if self.python_choice:
# Map python_choice back to a simple identifier if needed
py_ident = self.python_choice
if py_ident == '1':
py_ident = 'none'
elif py_ident == '2':
py_ident = 'dev'
else:
py_ident = f'py{py_ident}' # e.g., py7, py12
tag_parts.append(py_ident)
if self.py_pm_choice:
# Use the cleaned PM name (uv, poetry, pdm)
tag_parts.append(self.py_pm_choice)
# Add identifiers for other major choices if they are not defaults
if self.ubuntu_mirror_choice and self.ubuntu_mirror_choice != '1':
tag_parts.append(f'ubm{self.ubuntu_mirror_choice}') # ubm2, ubm3
if self.pip_mirror_choice and self.pip_mirror_choice != '1':
tag_parts.append(f'pm{self.pip_mirror_choice}') # pm2, pm3
if self.extra_python_pkg_choice and self.extra_python_pkg_choice != '1':
tag_parts.append(f'pext{self.extra_python_pkg_choice}') # pext2, pext3
if self.selected_extra_packages:
# Add a hash or abbreviated list for extra packages if too long
extra_pkgs_str = "_".join(sorted(self.selected_extra_packages)).replace('-', '') # make_curl_wget -> makecurlwget
if extra_pkgs_str:
tag_parts.append(f'extpkgs-{extra_pkgs_str}')
return '-'.join(tag_parts).lower() # Use hyphens for clarity
def dockerfile_is_exists(self):
return os.path.exists("Dockerfile")
def gen(self):
"""生成 Dockerfile 文件"""
if self.dockerfile_is_exists():
if not (self.args and self.args.force): # Allow forcing with argument
choice = input(f"{self.RED}Dockerfile已存在,是否覆盖?(y/n): {self.NC}")
if choice.lower() != 'y':
print(f"{self.RED}已取消操作{self.NC}")
return False
else:
os.remove("Dockerfile")
print(f"{self.YELLOW}已删除旧的Dockerfile{self.NC}")
else:
os.remove("Dockerfile")
print(f"{self.YELLOW}Dockerfile已存在,通过命令行参数覆盖。{self.NC}")
self.select_ubuntu_version()
self.select_ubuntu_mirror()
self.set_default_timezone()
self.select_python_version()
if self.python_choice != "1":
self.select_python_pip_mirror()
self.select_python_pm()
self.select_python_extra()
self.install_extra_packages()
self.clear_apt_cache()
self.read_dockerfile()
return True
class MakefileGenerator:
def __init__(self, tag_name, py_pm_choice):
self.py_pm_choice = py_pm_choice
self.makefile_content = f"""IMAGE_TAG := {tag_name}
WORK_DIR := /root/project
CACHE_DIR := /root/.cache
OUT_WORK_DIR := $(PWD)/project
OUT_CACHE_DIR := $(PWD)/__env_cache__
DOCKER_RUN := docker run \\
--rm -it \\
-v $(OUT_WORK_DIR):$(WORK_DIR) \\
-v $(OUT_CACHE_DIR):$(CACHE_DIR) \\
-w $(WORK_DIR) \\
--net=host \\
$(IMAGE_TAG)
build:
@docker build . -t $(IMAGE_TAG)
bash:
@$(DOCKER_RUN) /bin/bash
"""
self.makefile_content_uv = f"""
version:
@$(DOCKER_RUN) uv version
@$(DOCKER_RUN) uv run python -V
init:
@$(DOCKER_RUN) uv init
install:
@$(DOCKER_RUN) uv sync
add:
@$(DOCKER_RUN) uv add $(name)
remove:
@$(DOCKER_RUN) uv remove $(name)
run:
@$(DOCKER_RUN) uv run $(name)
"""
self.makefile_content_poetry = f"""
version:
@$(DOCKER_RUN) poetry run python -V
init:
@$(DOCKER_RUN) poetry init
install:
@$(DOCKER_RUN) poetry install --no-root
add:
@$(DOCKER_RUN) poetry add $(name)
remove:
@$(DOCKER_RUN) poetry remove $(name)
run:
@$(DOCKER_RUN) poetry run python $(name)
tests_env:
@$(DOCKER_RUN) poetry add --group dev pytest pytest-asyncio
tests:
@$(DOCKER_RUN) poetry run pytest tests.py
"""
self.makefile_content_pdm = f"""
version:
@$(DOCKER_RUN) pdm run python -V
init:
@$(DOCKER_RUN) pdm init
install:
@$(DOCKER_RUN) pdm install --no-self
add:
@$(DOCKER_RUN) pdm add $(name)
run:
@$(DOCKER_RUN) pdm run python $(name)
remove:
@$(DOCKER_RUN) pdm remove $(name)
"""
def gen(self):
"""生成 Makefile 文件"""
if self.py_pm_choice == "uv":
makefile_content = self.makefile_content + self.makefile_content_uv
elif self.py_pm_choice == "poetry":
makefile_content = self.makefile_content + self.makefile_content_poetry
elif self.py_pm_choice == "pdm":
makefile_content = self.makefile_content + self.makefile_content_pdm
else: # Default or pip
makefile_content = self.makefile_content # No extra PM commands for pip
with open("Makefile", "w") as f:
f.write(makefile_content)
print(f"{DockerfileGenerator.BLUE}Makefile已生成,内容如下:{DockerfileGenerator.NC}")
print(makefile_content)
def main():
parser = argparse.ArgumentParser(description="Generate Dockerfile and Makefile interactively or via arguments.")
parser.add_argument("--ubuntu", choices=["20", "22", "24"], help="Specify Ubuntu version (20, 22, 24)")
parser.add_argument("--ubuntu-mirror", choices=["1", "2", "3"], help="Specify Ubuntu mirror (1: official, 2: USTC, 3: Aliyun)")
parser.add_argument("--python", choices=["1", "2", "7", "8", "9", "10", "11", "12", "13"], help="Specify Python version or option (1: none, 2: dev, 7-13: specific version)")
parser.add_argument("--pip-mirror", choices=["1", "2", "3"], help="Specify pip mirror (1: none, 2: Tsinghua, 3: SUSTech)")
parser.add_argument("--pm", choices=["1", "2", "3", "4"], help="Specify Python package manager (1: pip, 2: uv, 3: poetry, 4: pdm)")
parser.add_argument("--extra-python-pkg", choices=["1", "2", "3"], help="Specify extra Python related packages (1: none, 2: selenium/chromedriver, 3: .NET SDK)")
parser.add_argument("--extra-packages", nargs='*', help="Specify additional apt packages (e.g., make curl wget libpq-dev)")
parser.add_argument("-f", "--force", action="store_true", help="Force overwrite existing Dockerfile and Makefile")
args = parser.parse_args()
dg = DockerfileGenerator(args=args)
if dg.gen():
print(f"{DockerfileGenerator.BLUE}镜像标签: {dg.image_tag}{DockerfileGenerator.NC}")
# Generate Makefile only if Dockerfile was successfully generated
mg = MakefileGenerator(dg.image_tag, dg.py_pm_choice)
mg.gen()
# Generate and print the shortcut command based on the final choices
command_parts = [sys.argv[0]] # script name
# Add arguments based on final choices (from args or interactive)
if dg.ubuntu_choice:
command_parts.append(f"--ubuntu {dg.ubuntu_choice}")
if dg.ubuntu_mirror_choice:
command_parts.append(f"--ubuntu-mirror {dg.ubuntu_mirror_choice}")
if dg.python_choice:
command_parts.append(f"--python {dg.python_choice}")
if dg.pip_mirror_choice:
command_parts.append(f"--pip-mirror {dg.pip_mirror_choice}")
if dg.py_pm_choice_key: # Use the key stored from selection
command_parts.append(f"--pm {dg.py_pm_choice_key}")
if dg.extra_python_pkg_choice:
command_parts.append(f"--extra-python-pkg {dg.extra_python_pkg_choice}")
if dg.selected_extra_packages:
command_parts.append("--extra-packages")
command_parts.extend(dg.selected_extra_packages) # These are already names
if args.force:
command_parts.append("--force")
print("\n")
print(f"{DockerfileGenerator.YELLOW}快捷命令参数提示:{DockerfileGenerator.NC}")
print(f"{' '.join(command_parts)}")
if __name__ == "__main__":
main()