もう 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
であれば環境變數を使って、
---
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 記:さっちゃんですよ ☆と餘り替はってゐない。