c4se記:さっちゃんですよ☆

.。oO(さっちゃんですよヾ(〃l _ l)ノ゙☆)

.。oO(此のblogは、主に音樂考察Programming に分類されますよ。ヾ(〃l _ l)ノ゙♬♪♡)

音樂は SoundCloud に公開中です。

考察は現在は主に Cosense で公表中です。

Programming は GitHub で開發中です。

WSL でも macOS でも、Docker で開發する

もう 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.pyWindowsmacOS 或いは 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 内であるか否かはunameLinuxMicrosoftを含む事で判定する。即ち、

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()と云ふ函數が見える。これは PowershellmacOS からなら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 を入れなくて好い。dockerdocker.exeでありkubectlkubect.exeだ。

殘る問題は Docker VOLUME を mount する事だ。local 開發環境なので手元の file を VOLUME で同期したい。通常は-v "$(pwd):/mnt"で path を指定する。WSL は pwd/mnt/c/〜だと言ふ。Docker for Windows は WSL ではなく Windows で動くので pwdC:\〜即ち/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を、静的檢査はflake8flake8-docstringsmypyとを (flake8-mypy は御亡くなりだ)、unit test にはpython -m unittest discover -sを使ってゐる。これはPython の unittest を書かう。其れを自動実行しやう (rake test + guard を置き換へやう) - c4se 記:さっちゃんですよ ☆と餘り替はってゐない。