もう WSL 2 も出ると云ふのに WSL 1 の話だが。
Python 3 で開發するとしやう。開發環境を Docker container に閉じ込めたい。と云ふのも Python の依存 library 管理は pip や Pipenv、Poetry と未だ固まってゐず、手元の環境は即座に捨てられるのが望ましい。Python 自體は運用しない事にする。詰り Python 3 と Git と Docker for Desktop とを手元に入れたら、開發出來る樣にする。Windows では WSL を使ってもらふ。
requirements.txt + pip でも好いのだが、ver. lock は當然行いたいし更新を樂にしたいから Poetry を Docker に入れる。
# poetry.toml [virtualenvs] create = false
FROM python:3-alpine SHELL ["/bin/ash", "-ex", "-o", "pipefail", "-c"] WORKDIR /mnt VOLUME /mnt ENV PATH=/root/.poetry/bin:$PATH RUN apk add --no-cache -t .build-deps \ curl \ && apk add --no-cache -t .runtime-deps \ build-base \ git \ python3-dev \ && curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py | python \ && apk del --purge .build-deps \ && rm -rf /var/cache/apk/* COPY poetry.lock poetry.toml pyproject.toml ./ RUN poetry install
概ねこう成るのではないか。apk add
は足りないかもしれない。venv を Docker VOLUME ではなく container 内に閉じ込めてゐるのは、Docker VOLUME に置くと Poetry が error を吐く爲…。docker-sync 藝も效かない。
さて task runner が要る。これはClojure で task runner を書く - c4se 記:さっちゃんですよ ☆で書いた。題は Clojure だが Python の事も全て書いた。手元には依存 library を入れないからこれはInvoke等を使はず標準 library だけで書く。
この tasks.py
は Windows、macOS 或いは Docker 内の sh から叩かれる。よってどこから叩かれたかを判定し處理を分ける事が要る。殆どは Docker 内で行ふから Docker 内であればそのまま、外であればdocker run
を叩く。
Docker container 内であるか否かは./dockerenv
が有るか、或いは手づから環境變數を設定してそれで判別する。即ち、
import os import os.path def within_docker() -> bool: """Detect I'm in a Docker or not.""" return os.path.exists("/.dockerenv") or os.getenv("CI") == "1"
WSL 内であるか否かはuname
がLinux
とMicrosoft
を含む事で判定する。即ち、
import platform def within_wsl() -> bool: """Detect I'm in a WSL or not.""" uname = platform.uname() return uname[0] == "Linux" and "Microsoft" in uname[2]
run
と云ふ函數で shell command を叩けるとして、こう使ひたい。
run("echo どの環境でもそのまま叩く") with docker() as _run: _run("echo Docker内で叩く") with powershell() as _run: _run("echo WSL内ならPowershell内で、macOSかDocker内ならそのまま叩く")
それには contextmanager を使ひこうする。
from contextlib import contextmanager from shlex import quote import re import subprocess @contextmanager def docker(): """Run command in Docker.""" if within_docker(): yield run else: yield run_in_docker @contextmanager def powershell(): """Run command in PowerShell if it's present.""" if within_wsl(): yield run_in_powershell else: yield run def run(command: str, capture_output=False, text=None) -> subprocess.CompletedProcess: """Run command.""" command = command.strip() print("+ ", command) return subprocess.run( command, capture_output=capture_output, check=True, shell=True, text=text, ) def run_in_docker( command: str, docker_options="", capture_output=False, text=None ) -> subprocess.CompletedProcess: """Run command in Docker.""" command = command.strip() print("+ ", command) return subprocess.run( f"{docker_compose_exe()} {docker_options} run --rm web {command}", capture_output=capture_output, check=True, shell=True, text=text, ) def run_in_powershell( command: str, capture_output=False, text=None ) -> subprocess.CompletedProcess: """Run command in PowerShell if it's present.""" command = re.sub(r"\\\n", r"`\n", command.strip()) print("+ ", command) return subprocess.run( f"powershell.exe -Command {quote(command)}", capture_output=capture_output, check=True, shell=True, text=text, )
bash は\
で改行を escape するが Powershell は`
で行ふ。
docker_compose_exe()
と云ふ函數が見える。これは Powershell や macOS からならdocker-compose
で呼べるが、WSL からだとdocker-compose.exe
でなければ呼べないからで、次の函數である。
def docker_compose_exe() -> str: """Get a DockerCompose executable name.""" if within_wsl(): return "docker-compose.exe" else: return "docker-compose"
これで WSL に Docker を入れなくて好い。docker
もdocker.exe
でありkubectl
もkubect.exe
だ。
殘る問題は Docker VOLUME を mount する事だ。local 開發環境なので手元の file を VOLUME で同期したい。通常は-v "$(pwd):/mnt"
で path を指定する。WSL は pwd を/mnt/c/〜
だと言ふ。Docker for Windows は WSL ではなく Windows で動くので pwd をC:\〜
即ち/c/〜
だと言ふ。この食ひ違ひは直さなければならない。そして同じ command が macOS でも動かなければならない。これには WSL の/mnt/c
を/c
に mount し直さなくても好い。
def cwd_for_docker_volume() -> str: """Get current directory for Docker volume. This works both on macOS & WSL.""" cwd = os.getcwd() if cwd.startswith("/mnt/c"): cwd = cwd[4:] return cwd
これで Docker for Desktop が使ふ pwd が得られる。docker run
であればこれを使って command を組み立てる。docker-compose run
であれば環境變數を使って、
--- # docker-compose.yml version: "3.7" services: web: # ... volumes: - ${PWD:-.}:/mnt:cached
def run_in_docker( command: str, docker_options="", capture_output=False, text=None ) -> subprocess.CompletedProcess: """Run command in Docker.""" command = command.strip() print("+ ", command) env = os.environ.copy() env["PWD"] = cwd_for_docker_volume() return subprocess.run( f"{docker_compose_exe()} {docker_options} run --rm web {command}", env=env, capture_output=capture_output, check=True, shell=True, text=text, )
と指定できる。
實際には更にElixir on Containers - Speaker Deckで紹介した rsync + unison 藝を行ふべきだ。
formatter にはBlackを、静的檢査はflake8とflake8-docstringsとmypyとを (flake8-mypy は御亡くなりだ)、unit test にはpython -m unittest discover -s
を使ってゐる。これはPython の unittest を書かう。其れを自動実行しやう (rake test + guard を置き換へやう) - c4se 記:さっちゃんですよ ☆と餘り替はってゐない。