From d03f230de1d0bf4117edf3ad992e69883cfa0971 Mon Sep 17 00:00:00 2001 From: "takemi.ohama" Date: Sat, 3 Oct 2026 12:41:35 +0900 Subject: [PATCH 1/4] =?UTF-8?q?refactor(containers):=20base=20=E3=81=8B?= =?UTF-8?q?=E3=82=89=20Playwright=20=E3=81=AE=20Chromium=E3=83=BB=E8=BF=BD?= =?UTF-8?q?=E5=8A=A0=E3=81=AE=E5=A4=AA=E3=81=95=E3=81=AE=E3=83=95=E3=82=A9?= =?UTF-8?q?=E3=83=B3=E3=83=88=E3=83=BBterraform=20=E3=82=92=E5=A4=96?= =?UTF-8?q?=E3=81=97=E3=80=81=E3=83=96=E3=83=A9=E3=82=A6=E3=82=B6=E3=81=AE?= =?UTF-8?q?=E6=B4=BE=E7=94=9F=E3=82=A4=E3=83=A1=E3=83=BC=E3=82=B8=20browse?= =?UTF-8?q?r=20=E3=82=92=E8=B6=B3=E3=81=99=20(#402)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit base は fonts-liberation・fonts-ipafont-gothic・fonts-wqy-zenhei を明示して入れ、 fc-match の解決先を保つ。HashiCorp の apt の取得元も外す。ブラウザの置き場と PLAYWRIGHT_BROWSERS_PATH は base に空のまま残し、containers/browser が Chromium・ 依存パッケージ・fonts-noto-cjk-extra を足す。 Co-Authored-By: Claude Opus 5.5 --- containers/base/Dockerfile | 51 +++++++++++++++++----------------- containers/bi-tools/Dockerfile | 2 +- containers/browser/Dockerfile | 35 +++++++++++++++++++++++ containers/browser/compose.yml | 7 +++++ 4 files changed, 68 insertions(+), 27 deletions(-) create mode 100644 containers/browser/Dockerfile create mode 100644 containers/browser/compose.yml diff --git a/containers/base/Dockerfile b/containers/base/Dockerfile index f6531212..be12a8dd 100644 --- a/containers/base/Dockerfile +++ b/containers/base/Dockerfile @@ -23,7 +23,11 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ openssh-client \ curl ca-certificates gnupg lsb-release \ libnss3 libxrandr2 libxss1 \ - fonts-noto-cjk fonts-noto-cjk-extra \ + fonts-noto-cjk \ + # fc-match の解決先 (Arial -> Liberation Sans、WenQuanYi Zen Hei・IPAPGothic の名指し) を保つ。 + # Playwright の --with-deps に頼らず明示する。追加の太さの fonts-noto-cjk-extra と + # Chromium はブラウザの派生イメージ (containers/browser) に置く (#402) + fonts-liberation fonts-ipafont-gothic fonts-wqy-zenhei \ # 欧文の metric 互換 (Calibri -> Carlito / Cambria -> Caladea)。 # 30-metric-aliases.conf が既に対応を持っており、実体が無いと中国語フォントへ落ちる。 fonts-crosextra-carlito fonts-crosextra-caladea \ @@ -45,11 +49,7 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ chmod a+r /etc/apt/keyrings/docker.gpg; \ echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" \ > /etc/apt/sources.list.d/docker.list; \ - # HashiCorp(Terraform) - curl -fsSL https://apt.releases.hashicorp.com/gpg \ - | gpg --dearmor -o /etc/apt/keyrings/hashicorp-archive-keyring.gpg; \ - echo "deb [signed-by=/etc/apt/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" \ - > /etc/apt/sources.list.d/hashicorp.list; \ + # terraform と HashiCorp の取得元は入れない。要るプロジェクトは自分の Dockerfile かフックで入れる (#402) # GitHub CLI リポジトリ curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg \ | dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg; \ @@ -57,15 +57,16 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ > /etc/apt/sources.list.d/github-cli.list; \ # NodeSource リポジトリ (Active LTS を追従。バージョン固定しない) curl -fsSL https://deb.nodesource.com/setup_lts.x | bash -; \ - # ブラウザは両アーキとも Playwright の Chromium だけを入れる (下の利用者の RUN が - # /opt/ms-playwright へ取得する)。システムの Chrome (google-chrome-stable) は入れない (#401)。 - # Ubuntu の chromium-browser は snap スタブでコンテナの中では起動しないため入れない (#220) + # base はブラウザを入れない。Playwright の Chromium とその依存パッケージはブラウザの派生イメージ + # (containers/browser) が /opt/ms-playwright へ取得する (#402)。システムの Chrome + # (google-chrome-stable) は入れない (#401)。Ubuntu の chromium-browser は snap スタブで + # コンテナの中では起動しないため入れない (#220) apt-get update; \ # パッケージインストール apt-get install -y --no-install-recommends \ docker-ce-cli \ docker-buildx-plugin docker-compose-plugin \ - terraform gh nodejs; \ + gh nodejs; \ # クリーンアップ。/usr/share/doc は copyright とディレクトリを指す symlink # (copyright への経路) だけを残す。除外の設定より前から ubuntu:26.04 にある物も消す (#400) apt-get clean; \ @@ -105,9 +106,10 @@ RUN set -eux; \ ENV NPM_CONFIG_PREFIX=/usr/local/share/npm-global ENV PATH="/usr/local/share/npm-global/bin:${PATH}" -# Playwright のブラウザの置き場。~/.cache の既定の置き場は下の片付けで消えるため、 -# 片付けの対象外の /opt に置いてイメージに残す (#220)。ENV にするのは docker exec の -# 非対話の処理からも同じ置き場を使うため。 +# Playwright のブラウザの置き場。base では空のまま作り、中身 (Chromium) はブラウザの派生 +# イメージ (containers/browser) が持つ (#402)。~/.cache の既定の置き場は片付けで消えるため、 +# 片付けの対象外の /opt に置く (#220)。ENV にするのは docker exec の非対話の処理と、 +# 実行時に取得する道具からも同じ置き場を使うため。 ENV PLAYWRIGHT_BROWSERS_PATH=/opt/ms-playwright # AWS CLI + gcloud SDK + npm globals(1レイヤーにまとめる) @@ -116,8 +118,8 @@ RUN set -eux; \ # 後から誰が入れてもグループが npm のまま揃う # (UID/GID を振り直しても壊れない Dev Containers 方式)。 install -d -m 2775 -o "$USERNAME" -g npm "$NPM_CONFIG_PREFIX"; \ - # Playwright のブラウザの置き場も同じ形で作る。下の利用者の RUN が取得し、 - # playwright-kit などが実行時に別の版を同じ置き場へ取得する (#220) + # Playwright のブラウザの置き場も同じ形で作る。ブラウザの派生イメージが取得し、 + # playwright-kit などが実行時に別の版を同じ置き場へ取得する (#220, #402) install -d -m 2775 -o "$USERNAME" -g npm "$PLAYWRIGHT_BROWSERS_PATH"; \ # AWS CLI v2 curl -fsSL "https://awscli.amazonaws.com/awscli-exe-linux-$(uname -m).zip" -o /tmp/awscliv2.zip; \ @@ -200,7 +202,7 @@ RUN gh --version && node --version && npm --version && aws --version && gcloud - USER ${USERNAME} WORKDIR /tmp -# ユーザー用ツール(uv, claude, kiro, playwright) +# ユーザー用ツール(uv, claude, agy, kiro) RUN set -eux; \ # uv for user curl -LsSf https://astral.sh/uv/install.sh | sh; \ @@ -226,12 +228,9 @@ RUN set -eux; \ else \ echo "Unsupported architecture: $ARCH, skipping Kiro CLI installation"; \ fi; \ - # Playwright の Chromium。置き場 ($PLAYWRIGHT_BROWSERS_PATH) は上の root の RUN が作る (#220) - npx playwright install --with-deps chromium; \ - # クリーンアップ。--with-deps の sudo apt や Node24/npm11 の - # /tmp/node-compile-cache が root 所有ファイルを残すため sudo で削除する - # (非 root の rm では Permission denied になりビルドが失敗する)。 - # --with-deps の apt-get update が残す apt の一覧と、/usr/share/doc の copyright でない物も消す (#400)。 + # クリーンアップ。Node24/npm11 の /tmp/node-compile-cache などが root 所有ファイルを + # 残すため sudo で削除する (非 root の rm では Permission denied になりビルドが失敗する)。 + # apt の一覧と、/usr/share/doc の copyright でない物も消す (#400)。 # ブラウザの置き場 ($PLAYWRIGHT_BROWSERS_PATH) は消さない。 sudo find /usr/share/doc -mindepth 1 ! -type d ! -xtype d ! -name copyright -delete; \ sudo rm -rf /tmp/* \ @@ -295,10 +294,10 @@ RUN sudo ln -sf tmux-first /usr/local/bin/tmux1 \ # 先頭のコメントにある。利用者は ~/.config/fontconfig/fonts.conf (スロット 50) で上書きできる。 COPY --chmod=0644 fonts-local.conf /etc/fonts/local.conf -# フォントのキャッシュを作り直す。上の Playwright の RUN が --with-deps で -# fonts-wqy-zenhei / fonts-ipafont-gothic / fonts-liberation を入れた「後」でなければ、 -# 後から入った書体を知らないキャッシュが残る。直後に ~/.cache を消しているため、 -# ここで /var/cache/fontconfig を作り直してコンテナ初回起動時の生成を避ける。 +# フォントのキャッシュを作り直す。上の local.conf の COPY の「後」に置き、利用者の RUN が +# ~/.cache を消した後でもキャッシュが残るよう、ここで /var/cache/fontconfig を作り直して +# コンテナ初回起動時の生成を避ける。ブラウザの派生イメージは足したフォントのために +# 自分でもう一度作り直す (#402)。 RUN sudo fc-cache -f # entrypoint diff --git a/containers/bi-tools/Dockerfile b/containers/bi-tools/Dockerfile index cfd1d441..db3cc42a 100644 --- a/containers/bi-tools/Dockerfile +++ b/containers/bi-tools/Dockerfile @@ -7,7 +7,7 @@ # - Python パッケージマネージャ uv(ubuntu ユーザにインストール済み) # - gcloud SDK(`bq` / `gcloud` を含む) # - Node.js LTS(`node` / `npm` / `npx` / `yarn`) -# - Docker(DooD), gh, terraform, aws-cli, git 等 +# - Docker(DooD), gh, aws-cli, git 等 # # BIツール(tools/{evidence,lightdash,superset})の起動形態と CLI の扱い: # - Lightdash : サーバは tools/lightdash の docker-compose で起動。ただし diff --git a/containers/browser/Dockerfile b/containers/browser/Dockerfile new file mode 100644 index 00000000..75fc9b85 --- /dev/null +++ b/containers/browser/Dockerfile @@ -0,0 +1,35 @@ +# ================================== +# devbase-browser: ブラウザと文書のフォント +# devbase-base + Playwright の Chromium・ブラウザの依存パッケージ・追加の太さのフォント +# ================================== +# base はブラウザを持たない (#402)。ブラウザを動かす・HTML を PDF やスクリーンショットにする +# プロジェクトは、dev サービスの build.context でこのイメージを選ぶ。 +# ブラウザの置き場 (/opt/ms-playwright) と ENV PLAYWRIGHT_BROWSERS_PATH は base から継ぐため、 +# ここでは宣言しない。base のフォントの規則 (/etc/fonts/local.conf) も書き換えずに継ぐ。 + +FROM devbase-base:latest + +# 追加の太さのフォント (Noto CJK の Thin・Light・Medium・Black など)。標準の太さは base の +# fonts-noto-cjk にある。標準のアーカイブのパッケージで、外部の取得元は要らない +USER root +RUN set -eux; \ + apt-get update; \ + apt-get install -y --no-install-recommends fonts-noto-cjk-extra; \ + apt-get clean; \ + rm -rf /var/lib/apt/lists/* + +# Playwright の Chromium とその依存パッケージ。npx は base の @playwright/test の CLI を使うため、 +# 版は base の @playwright/test に揃う。置き場は base の root の RUN が ubuntu:npm・2775 で作っている。 +# 片付けは --with-deps の sudo apt が残す root 所有の物も消すため sudo で行い、 +# ブラウザの置き場 ($PLAYWRIGHT_BROWSERS_PATH) とその親の /opt は消さない +USER ubuntu +RUN set -eux; \ + npx playwright install --with-deps chromium; \ + sudo find /usr/share/doc -mindepth 1 ! -type d ! -xtype d ! -name copyright -delete; \ + sudo rm -rf /tmp/* \ + /var/lib/apt/lists/* \ + ~/.cache \ + ~/.npm + +# 上の 2 つの RUN が入れたフォントをキャッシュへ載せる +RUN sudo fc-cache -f diff --git a/containers/browser/compose.yml b/containers/browser/compose.yml new file mode 100644 index 00000000..536d2ebf --- /dev/null +++ b/containers/browser/compose.yml @@ -0,0 +1,7 @@ +services: + + dev: + image: devbase-browser:latest + build: + context: . + dockerfile: Dockerfile From 705bece20c04d0bd6692627ca87a56da8cfda828 Mon Sep 17 00:00:00 2001 From: "takemi.ohama" Date: Sat, 3 Oct 2026 12:44:34 +0900 Subject: [PATCH 2/4] =?UTF-8?q?test(containers):=20base=20=E3=81=AB?= =?UTF-8?q?=E3=83=96=E3=83=A9=E3=82=A6=E3=82=B6=E3=81=8C=E7=84=A1=E3=81=84?= =?UTF-8?q?=E5=BD=A2=E3=81=A8=E3=83=96=E3=83=A9=E3=82=A6=E3=82=B6=E3=81=AE?= =?UTF-8?q?=E6=B4=BE=E7=94=9F=E3=82=A4=E3=83=A1=E3=83=BC=E3=82=B8=E3=81=AE?= =?UTF-8?q?=E6=A4=9C=E6=9F=BB=E3=81=B8=E7=A7=BB=E3=81=99=20(#402)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit base の形の検査を、Chromium・fonts-noto-cjk-extra・terraform・HashiCorp の取得元が無く、 解決先を保つ 3 つのフォントを明示する形に直す。#220 の受け入れ条件 5〜9 の検査は test_browser_image.py へ移し、DEVBASE_TEST_BROWSER_IMAGE で差し替える。建てた base の 検査 (test_base_image_without_browser.py)、browser の Dockerfile の形の検査、 devbase build が base を先に建てる経路の検査を足す。 Co-Authored-By: Claude Opus 5.5 --- tests/cli/test_build_browser_image.py | 50 +++++++ .../containers/test_base_dockerfile_fonts.py | 27 ++-- .../test_base_dockerfile_playwright.py | 123 ++++++++------- .../test_base_image_without_browser.py | 140 ++++++++++++++++++ tests/containers/test_browser_dockerfile.py | 115 ++++++++++++++ ...image_browser.py => test_browser_image.py} | 49 ++++-- 6 files changed, 431 insertions(+), 73 deletions(-) create mode 100644 tests/cli/test_build_browser_image.py create mode 100644 tests/containers/test_base_image_without_browser.py create mode 100644 tests/containers/test_browser_dockerfile.py rename tests/containers/{test_base_image_browser.py => test_browser_image.py} (77%) diff --git a/tests/cli/test_build_browser_image.py b/tests/cli/test_build_browser_image.py new file mode 100644 index 00000000..a06534d6 --- /dev/null +++ b/tests/cli/test_build_browser_image.py @@ -0,0 +1,50 @@ +"""ブラウザの派生イメージを選んだプロジェクトの `devbase build` が base を先に建てる (#402 受け入れ条件 10)。 + +dev の `build.context` が `${DEVBASE_ROOT}/containers/browser` のプロジェクトで `bin/devbase build` を +実プロセスで起動する。`containers/browser/Dockerfile` は本物を複製し、最初の `FROM` から +`devbase-base` を見つけて、base を建ててから `docker compose build dev` を打つことを固定する。 +Docker は起動しない (外への呼び出しは偽の `uv` が受ける)。 +""" + +from __future__ import annotations + +import shutil + +from tests.cli.conftest import REPO_ROOT + +BROWSER_DOCKERFILE = REPO_ROOT / "containers" / "browser" / "Dockerfile" + + +def _uv_lines(result): + return [line[len("UV:"):] for line in result.stdout.splitlines() if line.startswith("UV:")] + + +def _browser_project(exec_wrapper): + exec_wrapper.container("base") + browser = exec_wrapper.root / "containers" / "browser" + browser.mkdir() + shutil.copy(BROWSER_DOCKERFILE, browser / "Dockerfile") + project = exec_wrapper.work / "myproj" + project.mkdir() + (project / "compose.yml").write_text( + "services:\n" + " dev:\n" + " build:\n" + " context: ${DEVBASE_ROOT}/containers/browser/\n" + " dockerfile: Dockerfile\n" + ) + return project + + +def test_build_of_a_browser_project_builds_base_before_the_project(exec_wrapper): + project = _browser_project(exec_wrapper) + + result = exec_wrapper.run(["build"], cwd=project) + + assert result.returncode == 0, result.stdout + result.stderr + assert "Project uses devbase-base" in result.stdout + uv = _uv_lines(result) + base = [i for i, line in enumerate(uv) if "buildx build --load -t devbase-base:latest" in line] + project_build = [i for i, line in enumerate(uv) if line.endswith("docker compose build dev")] + assert base and project_build, result.stdout + assert base[0] < project_build[0], result.stdout diff --git a/tests/containers/test_base_dockerfile_fonts.py b/tests/containers/test_base_dockerfile_fonts.py index ac40925e..0df33109 100644 --- a/tests/containers/test_base_dockerfile_fonts.py +++ b/tests/containers/test_base_dockerfile_fonts.py @@ -6,9 +6,11 @@ ここで固定するのは次の 5 つである。 -- 6 パッケージが **1 つ目の RUN の 1 回目の** ``apt-get install`` の一覧にある (設計の決定 4) +- 6 パッケージと、解決先を保つ 3 つのフォント (#402) が **1 つ目の RUN の 1 回目の** + ``apt-get install`` の一覧にある (設計の決定 4) - ``COPY`` の宛先が ``/etc/fonts/local.conf`` であり ``conf.d/`` ではない (決定 1) -- ``fc-cache -f`` が 1 度だけ、``COPY`` より後、かつ Playwright の ``RUN`` より後にある (決定 5) +- ``fc-cache -f`` が 1 度だけ、``COPY`` より後にある (決定 5。base は Playwright の ``RUN`` を + 持たない。Playwright の後の順は ``test_browser_dockerfile.py`` が固定する) - ``fonts-local.conf`` が 4 つの ```` と 9 つの ```` を持ち、言語の規則が 総称ファミリの ```` を必ず伴う (決定 2。この ```` を省くと欧文の指定を奪う) - 入れないもの (LibreOffice / pip) と、消さないもの (``fonts-wqy-zenhei``) が守られている @@ -53,6 +55,13 @@ "fonts-crosextra-carlito", "fonts-crosextra-caladea", ) +# fc-match の解決先 (Arial -> Liberation Sans、WenQuanYi Zen Hei・IPAPGothic の名指し) を保つ 3 つ。 +# Playwright の --with-deps に頼らず base が明示して入れる (#402) +RESOLVED_FONT_PACKAGES = ( + "fonts-liberation", + "fonts-ipafont-gothic", + "fonts-wqy-zenhei", +) def _statements() -> str: @@ -97,7 +106,7 @@ def _first_apt_install(block: str) -> str: """1 つ目の RUN の**1 回目**の apt-get install の一覧だけを取り出す 1 つ目の RUN は apt-get install を 2 回呼ぶ。1 回目は Ubuntu の標準のアーカイブから、 - 2 回目は後から足したリポジトリ (docker-ce-cli / terraform / gh / nodejs) からである。 + 2 回目は後から足したリポジトリ (docker-ce-cli / gh / nodejs) からである。 """ calls = [m.start() for m in re.finditer(r"apt-get install", block)] assert len(calls) >= 2, "1 つ目の RUN に apt-get install が 2 回無い" @@ -114,6 +123,12 @@ def test_the_six_packages_are_in_the_first_apt_install(package): assert re.search(rf"(? list[Instruction]: return parse(path.read_text()) +def _runs(path: Path) -> list[str]: + return [ins.args for ins in _instructions(path) if ins.keyword == "RUN"] + + +def _apt_installs(path: Path) -> list[str]: + """``apt-get install`` の一覧 (次の ``;`` か ``&&`` まで) を全部取り出す""" + found = [] + for run in _runs(path): + for command in re.split(r";|&&", run): + if "apt-get install" in command: + found.append(command) + assert found, "apt-get install が見つからない" + return found + + +def _word(name: str) -> re.Pattern[str]: + return re.compile(rf"(? list[int]: return [i for i, ins in enumerate(instructions) if ins.keyword == "ENV" and ins.args.startswith("PLAYWRIGHT_BROWSERS_PATH")] -def _install_indexes(instructions: list[Instruction]) -> list[int]: - return [i for i, ins in enumerate(instructions) - if ins.keyword == "RUN" and "npx playwright install" in ins.args] +# --- I1: base はブラウザ・追加の太さのフォント・terraform を持たない --- -def test_browsers_path_is_declared_once_before_the_install(): - instructions = _instructions(BASE) - envs = _env_indexes(instructions) - installs = _install_indexes(instructions) - assert len(envs) == 1 and installs - assert instructions[envs[0]].args == ENV_LINE - assert envs[0] < min(installs) +def test_base_does_not_install_the_playwright_chromium(): + for run in _runs(BASE): + assert "playwright install" not in run, "base の RUN が Playwright の Chromium を取得している" -def _base_install_run() -> str: - instructions = _instructions(BASE) - installs = _install_indexes(instructions) - assert len(installs) == 1 - return instructions[installs[0]].args +@pytest.mark.parametrize("package", ["fonts-noto-cjk-extra", "terraform"]) +def test_base_does_not_apt_install(package): + for command in _apt_installs(BASE): + assert not _word(package).search(command), f"base が {package} を apt で入れている" + + +def test_base_does_not_add_the_hashicorp_apt_source(): + for run in _runs(BASE): + assert "apt.releases.hashicorp.com" not in run, "base が HashiCorp の apt の取得元を足している" -def _rm_targets(run: str) -> list[str]: - targets: list[str] = [] - for command in re.split(r";|&&", run): - words = command.split() - if "rm" not in words: - continue - targets += [w for w in words[words.index("rm") + 1:] if not w.startswith("-")] - return targets +# --- I3: @playwright/test は残る --- -def test_base_install_keeps_with_deps(): - assert "npx playwright install --with-deps chromium" in _base_install_run() +def test_playwright_test_stays_in_the_npm_globals(): + globals_ = [command for run in _runs(BASE) for command in re.split(r";|&&", run) + if re.match(r"\s*npm (i|install) -g\b", command)] + assert globals_, "npm のグローバルの install が無い" + assert any("@playwright/test" in command.split() for command in globals_) -def test_the_cleanup_does_not_remove_the_browsers_path(): - targets = _rm_targets(_base_install_run()) - assert targets, "片付けの rm -rf が見つからない" - for target in targets: - cleaned = target.rstrip("/*").rstrip("/") - assert cleaned not in {"/opt", BROWSERS_PATH, "$PLAYWRIGHT_BROWSERS_PATH", - "${PLAYWRIGHT_BROWSERS_PATH}"}, target - assert not target.startswith(f"{BROWSERS_PATH}"), target +# --- I4: 空のブラウザの置き場 --- -def test_the_browsers_path_is_made_like_the_npm_prefix_before_the_install(): - """決定 1: 置き場は npm のグローバル領域と同じ root の RUN で、$USERNAME:npm・2775 で作る""" +def test_browsers_path_is_declared_once_before_it_is_made(): + """置き場は npm のグローバル領域と同じ root の RUN で、$USERNAME:npm・2775 で作る""" instructions = _instructions(BASE) + envs = _env_indexes(instructions) + assert len(envs) == 1, "ENV PLAYWRIGHT_BROWSERS_PATH が 1 つでない" + assert instructions[envs[0]].args == ENV_LINE made = re.compile(r'install -d -m 2775 -o "\$USERNAME" -g npm "\$PLAYWRIGHT_BROWSERS_PATH"') runs = [i for i, ins in enumerate(instructions) if ins.keyword == "RUN" and made.search(ins.args)] assert len(runs) == 1, "ブラウザの置き場を $USERNAME:npm・2775 で作る RUN が 1 つでない" assert "NPM_CONFIG_PREFIX" in instructions[runs[0]].args, "npm のグローバル領域と同じ RUN で作っていない" - assert _env_indexes(instructions)[0] < runs[0] < _install_indexes(instructions)[0] + assert envs[0] < runs[0] + + +# --- ブラウザの扱い (#220, #401) --- def test_base_does_not_install_chromium_browser(): - installs = [ins.args for ins in _instructions(BASE) - if ins.keyword == "RUN" and "apt-get install" in ins.args] - assert installs - for run in installs: - assert not re.search(r"(?\t\t` を出す。失敗しても止めずに全項目を採る +_PROBE = r""" +set -u +emit() { printf '%s\t%s\t%s\n' "$1" "$2" "$3"; } + +found="" +for d in '__PATH__'/chromium-* '__PATH__'/chromium_headless_shell-*; do + [ -e "$d" ] && found="$found ${d##*/}" +done +emit browsers chromium "${found# }" + +dpkg -s fonts-noto-cjk-extra >/dev/null 2>&1 +emit dpkg fonts-noto-cjk-extra "$?" +command -v terraform >/dev/null 2>&1 +emit command terraform "$?" + +work="$(mktemp -d)" +cat > "$work/launch.js" <<'JS' +const path = require('path'); +const root = require('child_process').execSync('npm root -g').toString().trim(); +const { chromium } = require(path.join(root, '@playwright/test')); +(async () => { + const browser = await chromium.launch({ headless: true }); + await browser.close(); +})().catch((e) => { console.error(String(e && e.message || e)); process.exit(1); }); +JS +node "$work/launch.js" >"$work/node.log" 2>&1 +emit launch exit "$?" +emit launch log "$(tail -c 4000 "$work/node.log" | tr '\t\n' ' ')" +exit 0 +""" + + +def _docker_unavailable() -> str | None: + if shutil.which("docker") is None: + return "docker が PATH に無い" + try: + subprocess.run(["docker", "info"], capture_output=True, timeout=30, check=True, + env=host_docker_env()) + except (subprocess.SubprocessError, OSError): + return "docker daemon へ繋がらない" + try: + out = subprocess.run(["docker", "image", "inspect", IMAGE], + capture_output=True, timeout=30, env=host_docker_env()) + except (subprocess.SubprocessError, OSError): + return f"{IMAGE} を調べられない" + if out.returncode != 0: + hint = "" if IMAGE_IS_EXPLICIT else f"。{BUILD_HINT}" + return f"{IMAGE} が無い{hint}" + return None + + +@pytest.fixture(scope="session") +def probe() -> dict[str, dict[str, str]]: + """1 回の docker run (--network none、利用者 ubuntu の非対話の bash -c) で値をまとめて採る""" + reason = _docker_unavailable() + if reason: + pytest.skip(reason) + out = subprocess.run( + ["docker", "run", "--rm", "--network", "none", "--user", USERNAME, + "--entrypoint", "/bin/bash", IMAGE, "-c", _PROBE.replace("__PATH__", BROWSERS_PATH)], + capture_output=True, text=True, timeout=300, env=host_docker_env()) + assert out.returncode == 0, f"probe が失敗した (exit={out.returncode}):\n{out.stderr}" + + collected: dict[str, dict[str, str]] = {} + for line in out.stdout.splitlines(): + if not line.strip(): + continue + kind, key, value = line.split("\t", 2) + collected.setdefault(kind, {})[key] = value + + if collected["browsers"]["chromium"] and not IMAGE_IS_EXPLICIT: + pytest.skip(f"{IMAGE} に Chromium がある (#402 より前のイメージ)。{BUILD_HINT}") + return collected + + +def test_the_browsers_path_has_no_chromium(probe): + """受け入れ条件 6。置き場に Chromium の版のディレクトリが無い""" + assert probe["browsers"]["chromium"] == "", f"置き場にある: {probe['browsers']['chromium']}" + + +def test_the_extra_weights_are_not_installed(probe): + """受け入れ条件 6""" + assert probe["dpkg"]["fonts-noto-cjk-extra"] != "0" + + +def test_terraform_is_not_installed(probe): + """受け入れ条件 6""" + assert probe["command"]["terraform"] != "0" + + +def test_launching_chromium_fails_and_names_the_next_step(probe): + """受け入れ条件 5。非 0 で終わり、出力に取得のコマンドかブラウザの派生イメージの名前がある""" + launch = probe["launch"] + assert launch["exit"] != "0", "base で Chromium が起動した" + assert any(step in launch["log"] for step in NEXT_STEPS), f"出力: {launch['log']}" diff --git a/tests/containers/test_browser_dockerfile.py b/tests/containers/test_browser_dockerfile.py new file mode 100644 index 00000000..92c46a6a --- /dev/null +++ b/tests/containers/test_browser_dockerfile.py @@ -0,0 +1,115 @@ +"""ブラウザの派生イメージ ``containers/browser`` の形 (#402) + +Docker を起動せず、``containers/browser/Dockerfile`` と ``compose.yml`` の形だけを固定する。 +建てたイメージの振る舞いは ``test_browser_image.py`` が固定する。 + +- 最初の ``FROM`` が ``devbase-base:latest`` (``devbase build`` が base を先に建てる経路に乗る) +- 利用者の ``RUN`` で ``npx playwright install --with-deps chromium`` を 1 回打ち、その片付けは + ブラウザの置き場とその親の ``/opt`` を消さず、``/var/lib/apt/lists`` を消す +- ``fonts-noto-cjk-extra`` を apt で入れ、``fc-cache -f`` は 1 度だけ、apt と Playwright の ``RUN`` より後 +- 置き場と ``ENV PLAYWRIGHT_BROWSERS_PATH`` は base から継ぎ、宣言し直さない +""" + +from __future__ import annotations + +import re +from pathlib import Path + +from .dockerfile_parse import Instruction, parse + +BROWSER_DIR = Path(__file__).resolve().parents[2] / "containers" / "browser" +DOCKERFILE = BROWSER_DIR / "Dockerfile" +COMPOSE = BROWSER_DIR / "compose.yml" +BROWSERS_PATH = "/opt/ms-playwright" +INSTALL = "npx playwright install --with-deps chromium" + + +def _instructions() -> list[Instruction]: + return parse(DOCKERFILE.read_text()) + + +def _run_indexes(instructions: list[Instruction], needle: str) -> list[int]: + return [i for i, ins in enumerate(instructions) if ins.keyword == "RUN" and needle in ins.args] + + +def _rm_targets(run: str) -> list[str]: + targets: list[str] = [] + for command in re.split(r";|&&", run): + words = command.split() + if "rm" not in words: + continue + targets += [w for w in words[words.index("rm") + 1:] if not w.startswith("-")] + return targets + + +def _user_at(instructions: list[Instruction], index: int) -> str: + """``index`` 番目の命令を実行する利用者 (直前の ``USER``。無ければ base の最後の ubuntu)""" + user = "ubuntu" + for ins in instructions[:index]: + if ins.keyword == "USER": + user = ins.args + return user + + +# --- I9 --- + + +def test_the_first_from_is_devbase_base(): + froms = [ins.args for ins in _instructions() if ins.keyword == "FROM"] + assert froms and froms[0] == "devbase-base:latest" + + +def test_compose_names_the_image_devbase_browser(): + text = COMPOSE.read_text() + assert re.search(r"^\s+image:\s*devbase-browser:latest\s*$", text, flags=re.MULTILINE) + assert re.search(r"^\s+context:\s*\.\s*$", text, flags=re.MULTILINE) + + +# --- I10 --- + + +def test_the_playwright_install_runs_once_as_the_user(): + instructions = _instructions() + installs = _run_indexes(instructions, "playwright install") + assert len(installs) == 1, "Playwright の取得の RUN が 1 つでない" + assert INSTALL in instructions[installs[0]].args + assert _user_at(instructions, installs[0]) == "ubuntu", "Playwright の取得を利用者 ubuntu で打っていない" + + +def test_the_cleanup_keeps_the_browsers_path_and_removes_apt_lists(): + instructions = _instructions() + run = instructions[_run_indexes(instructions, "playwright install")[0]].args + targets = _rm_targets(run) + assert targets, "片付けの rm -rf が見つからない" + for target in targets: + cleaned = target.rstrip("/*").rstrip("/") + assert cleaned not in {"/opt", BROWSERS_PATH, "$PLAYWRIGHT_BROWSERS_PATH", + "${PLAYWRIGHT_BROWSERS_PATH}"}, target + assert not target.startswith(BROWSERS_PATH), target + assert "/var/lib/apt/lists/*" in targets, "片付けが apt の一覧を消さない" + + +def test_the_browsers_path_is_inherited_not_redeclared(): + for ins in _instructions(): + assert not (ins.keyword == "ENV" and "PLAYWRIGHT_BROWSERS_PATH" in ins.args), \ + "置き場は base から継ぐ。ENV を宣言し直している" + + +# --- I11 --- + + +def test_the_extra_weights_are_installed_with_apt(): + applied = [ins.args for ins in _instructions() + if ins.keyword == "RUN" and re.search(r"apt-get install[^;&]*\bfonts-noto-cjk-extra\b", ins.args)] + assert len(applied) == 1, "fonts-noto-cjk-extra を apt で入れる RUN が 1 つでない" + assert "/var/lib/apt/lists/*" in _rm_targets(applied[0]), "apt の RUN が apt の一覧を消さない" + + +def test_fc_cache_runs_once_after_apt_and_playwright(): + instructions = _instructions() + caches = _run_indexes(instructions, "fc-cache -f") + assert len(caches) == 1, "fc-cache -f が 1 度でない" + apt = _run_indexes(instructions, "fonts-noto-cjk-extra") + playwright = _run_indexes(instructions, "playwright install") + assert apt and playwright + assert max(apt + playwright) < caches[0], "fc-cache が apt か Playwright の RUN より前にある" diff --git a/tests/containers/test_base_image_browser.py b/tests/containers/test_browser_image.py similarity index 77% rename from tests/containers/test_base_image_browser.py rename to tests/containers/test_browser_image.py index ef2a59ac..556b5371 100644 --- a/tests/containers/test_base_image_browser.py +++ b/tests/containers/test_browser_image.py @@ -1,21 +1,23 @@ -"""建てた base イメージのブラウザの置き場 (#220 の受け入れ条件 5〜9) +"""建てたブラウザの派生イメージ (#220 の受け入れ条件 5〜9、#402 の受け入れ条件 8・9) -Dockerfile の形の検査 (``test_base_dockerfile_playwright.py`` など) では足りない。固定したいのは +base はブラウザを持たない (#402)。Playwright の Chromium・ブラウザの依存パッケージ・追加の太さの +フォントはブラウザの派生イメージ ``devbase-browser`` が持つ。 + +Dockerfile の形の検査 (``test_browser_dockerfile.py`` など) では足りない。固定したいのは 「``ENV`` の行があること」ではなく「建てたイメージで Playwright の Chromium がネットワーク無しで 起動し、日本語のページを Noto Sans CJK JP で PDF にできること」で、後者は文字列からは分からない。 -**検査するイメージは ``DEVBASE_TEST_BASE_IMAGE`` で差し替える (設計の決定 3)。** 既定は -``devbase-base:latest``。マージ前の確認は ``devbase-base:latest`` を上書きしない別のタグを渡し、 +**検査するイメージは ``DEVBASE_TEST_BROWSER_IMAGE`` で差し替える (#220 の決定 3)。** 既定は +``devbase-browser:latest``。マージ前の確認は ``devbase-browser:latest`` を上書きしない別のタグを渡し、 リリース後テストでは既定のまま同じ検査を走らせる。 **skip するのは次の 3 つだけ。** - Docker が無い・daemon へ繋がらない - 検査するイメージが無い -- 既定の ``devbase-base:latest`` を見ていて、ブラウザの置き場 (``/opt/ms-playwright``) が無い - (この変更より前に建てたイメージ) +- 既定の ``devbase-browser:latest`` を見ていて、ブラウザの置き場 (``/opt/ms-playwright``) が無い -``DEVBASE_TEST_BASE_IMAGE`` を明示したときは、置き場が無くても skip せず落とす。置き場を +``DEVBASE_TEST_BROWSER_IMAGE`` を明示したときは、置き場が無くても skip せず落とす。置き場を 片付けで消して建てたイメージは、置き場の無い古いイメージと見分けられないため、明示したタグまで skip すると、テスト設計の「片付けに置き場を足して建てる」壊し方が skip で通ってしまう。 @@ -34,21 +36,24 @@ from tests.conftest import host_docker_env -DEFAULT_IMAGE = "devbase-base:latest" +DEFAULT_IMAGE = "devbase-browser:latest" # 隔離の fixture より前 (読み込み時) に読む。隔離の一覧に入っていないが、入っても効くように -_IMAGE_FROM_ENV = os.environ.get("DEVBASE_TEST_BASE_IMAGE", "").strip() +_IMAGE_FROM_ENV = os.environ.get("DEVBASE_TEST_BROWSER_IMAGE", "").strip() IMAGE = _IMAGE_FROM_ENV or DEFAULT_IMAGE IMAGE_IS_EXPLICIT = bool(_IMAGE_FROM_ENV) -BUILD_HINT = f"`devbase build base --no-cache` で {DEFAULT_IMAGE} を建て直すと、この検査が効く" +BUILD_HINT = f"`devbase build base` の後に `devbase build browser` で {DEFAULT_IMAGE} を建てると、この検査が効く" BROWSERS_PATH = "/opt/ms-playwright" USERNAME = "ubuntu" # 受け入れ条件 8。ブラウザの依存パッケージは減らない DEPENDENCY_PACKAGES = ("fonts-liberation", "fonts-ipafont-gothic", "fonts-wqy-zenhei", "libnss3") +# #402 の受け入れ条件 9。追加の太さのフォント +EXTRA_WEIGHTS_PACKAGE = "fonts-noto-cjk-extra" +EXTRA_WEIGHTS = ("Thin", "Black") # 並列でも probe (docker run) を 1 回で済ませるため、全ケースを同じワーカーへ割り当てる -pytestmark = pytest.mark.xdist_group("base_image_browser_probe") +pytestmark = pytest.mark.xdist_group("browser_image_probe") # 1 行 1 項目の `\t\t` を出す。失敗しても止めずに全項目を採る # (どの受け入れ条件が落ちたかをケースごとに分けて見せるため)。 @@ -96,6 +101,11 @@ emit dpkg "$pkg" "$?" done +# #402 の受け入れ条件 9。追加の太さのフォントのパッケージと、Noto Sans CJK JP の太さ +dpkg -s __EXTRA_PACKAGE__ >/dev/null 2>&1 +emit extra package "$?" +emit extra styles "$(fc-list ':family=Noto Sans CJK JP' style | tr '\n' ',')" + # 受け入れ条件 9 (後半)。Chromium を起動した後でも ~/.cache の置き場が無い if [ -e "$HOME/.cache/ms-playwright" ]; then emit home-cache '' present; else emit home-cache '' absent; fi exit 0 @@ -129,7 +139,8 @@ def probe() -> dict[str, dict[str, str]]: if reason: pytest.skip(reason) script = (_PROBE.replace("__PATH__", BROWSERS_PATH) - .replace("__PACKAGES__", " ".join(DEPENDENCY_PACKAGES))) + .replace("__PACKAGES__", " ".join(DEPENDENCY_PACKAGES)) + .replace("__EXTRA_PACKAGE__", EXTRA_WEIGHTS_PACKAGE)) # ここは包まない。Docker もイメージもある状態で probe がタイムアウトした・起動に失敗したのは # 「壊れている」ため、例外のまま失敗として知らせる out = subprocess.run( @@ -146,7 +157,7 @@ def probe() -> dict[str, dict[str, str]]: collected.setdefault(kind, {})[key] = value if collected["browsers-dir"][""] == "absent" and not IMAGE_IS_EXPLICIT: - pytest.skip(f"{IMAGE} に {BROWSERS_PATH} が無い (#220 より前のイメージ)。{BUILD_HINT}") + pytest.skip(f"{IMAGE} に {BROWSERS_PATH} が無い。{BUILD_HINT}") return collected @@ -193,3 +204,15 @@ def test_the_env_points_at_the_browsers_path(probe): def test_the_home_cache_has_no_browsers(probe): """受け入れ条件 9 (後半)。置き場が ~/.cache に戻っていない""" assert probe["home-cache"][""] == "absent" + + +def test_the_extra_weights_package_is_installed(probe): + """#402 の受け入れ条件 9 (前半)""" + assert probe["extra"]["package"] == "0", f"{EXTRA_WEIGHTS_PACKAGE} が入っていない" + + +@pytest.mark.parametrize("weight", EXTRA_WEIGHTS) +def test_noto_sans_cjk_jp_has_the_extra_weight(probe, weight): + """#402 の受け入れ条件 9 (後半)。fc-list の style は別名をコンマで連ねるため、語で見る""" + styles = {s.strip() for s in probe["extra"]["styles"].replace(":style=", ",").split(",")} + assert weight in styles, f"Noto Sans CJK JP の style: {probe['extra']['styles']!r}" From 6c6aa0f41a92910ab42554c92da8237b6621fe7d Mon Sep 17 00:00:00 2001 From: "takemi.ohama" Date: Sat, 3 Oct 2026 12:49:36 +0900 Subject: [PATCH 3/4] =?UTF-8?q?docs:=20base=20=E3=81=8B=E3=82=89=E3=83=96?= =?UTF-8?q?=E3=83=A9=E3=82=A6=E3=82=B6=E3=83=BB=E8=BF=BD=E5=8A=A0=E3=81=AE?= =?UTF-8?q?=E5=A4=AA=E3=81=95=E3=81=AE=E3=83=95=E3=82=A9=E3=83=B3=E3=83=88?= =?UTF-8?q?=E3=83=BBterraform=20=E3=82=92=E5=A4=96=E3=81=97=E3=81=9F?= =?UTF-8?q?=E3=81=93=E3=81=A8=E3=81=A8=20devbase-browser=20=E3=81=B8?= =?UTF-8?q?=E3=81=AE=E7=A7=BB=E3=82=8A=E6=96=B9=E3=82=92=E4=BB=95=E6=A7=98?= =?UTF-8?q?=E3=83=BB=E5=88=A9=E7=94=A8=E8=80=85=E3=81=AE=E6=96=87=E6=9B=B8?= =?UTF-8?q?=E3=83=BBCHANGELOG=20=E3=81=AB=E6=9B=B8=E3=81=8F=20(#402)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 24 ++- docs/specifications/base-image-rendering.md | 186 +++++++++++++------- docs/user/container-operations.md | 89 ++++++++-- 3 files changed, 213 insertions(+), 86 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 325d6da4..655cdd3c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,12 +25,11 @@ GPU を使うホストの NVIDIA ドライバは CUDA 13.4 以上に対応している必要があります(`nvidia-smi` の `CUDA Version` で確かめられます)。足りないと `--gpus all` の起動が requirement error で止まるので、 ドライバを最新に上げてから `devbase build lfm` で建て直してください。 -- **base イメージに Playwright の Chromium を残すようにしました(#220)。** これまではビルドで取得した - ブラウザを直後の片付けで捨てており、arm64(Apple Silicon)のコンテナには起動できる Chromium が - ありませんでした。ブラウザの置き場は `/opt/ms-playwright` で、環境変数 `PLAYWRIGHT_BROWSERS_PATH` が - 指します。実行時に `playwright install` で取得するブラウザも `~/.cache/ms-playwright` ではなくここへ - 入ります。起動できなかった `chromium-browser`(Ubuntu の snap スタブ)は外しました。base の後に - 派生イメージを建て直すと届きます。 +- **Playwright のブラウザの置き場を `/opt/ms-playwright` にしました(#220)。** 環境変数 + `PLAYWRIGHT_BROWSERS_PATH` が指し、実行時に `playwright install` で取得するブラウザも + `~/.cache/ms-playwright` ではなくここへ入ります。ビルドで取得した Chromium は片付けで消えず、 + arm64(Apple Silicon)でも起動できます。Chromium はブラウザの派生イメージ `devbase-browser` が持ちます + (#402)。起動できなかった `chromium-browser`(Ubuntu の snap スタブ)は外しました。 ### Removed - **派生イメージ `containers/lfm` を廃止しました(#403、破壊的変更)。** 使うプロジェクトが無く、 @@ -53,11 +52,22 @@ base を建て直し、派生イメージも建て直してください。 - **amd64 の base イメージからシステムの Chrome(`google-chrome-stable`)を外しました(#401、破壊的変更)。** Google の apt の取得元も足しません。arm64 にはもともと無く、ブラウザは両アーキとも Playwright の - Chromium(`/opt/ms-playwright`)にそろいます。amd64 の base は Chrome と依存を合わせて約 540MB + Chromium(`/opt/ms-playwright`。#402 からはブラウザの派生イメージ `devbase-browser` にあります)にそろいます。amd64 の base は Chrome と依存を合わせて約 540MB 小さくなります。Playwright から使う処理はそのまま動きます。`/usr/bin/google-chrome` を呼んでいた 道具には、`NODE_PATH="$(npm root -g)" node -e "console.log(require('@playwright/test').chromium.executablePath())"` で得られる Playwright の Chromium のパスを渡してください。反映には `devbase build base` で base を 建て直し、派生イメージも建て直してください。 +- **base イメージから Playwright の Chromium とその依存パッケージ・Noto CJK の追加の太さ + (`fonts-noto-cjk-extra`)・`terraform` を外しました(#402、破壊的変更)。** すべての利用者が使う道具では + なく、arm64 で合わせて 1GB を超えていました。HashiCorp の apt の取得元も足しません。`@playwright/test` と + ブラウザの置き場 `/opt/ms-playwright`(空)は base に残り、標準の太さの日本語・中国語・韓国語と欧文の + metric 互換の描画は変わりません。ブラウザと追加の太さのフォントは、新しい派生イメージ + `devbase-browser`(`containers/browser`)にあります。要るプロジェクトは dev サービスの `build.context` を + `${DEVBASE_ROOT}/containers/browser/` にしてください。`php` など別の派生イメージの道具も要るとき、 + `terraform` を入れるときの手順は + [コンテナ操作ガイド: ブラウザ・追加の太さのフォント・terraform を使う](docs/user/container-operations.md#ブラウザ追加の太さのフォントterraform-を使う) + にあります。反映には `devbase build base` で base を、続けて使っている派生イメージを建て直し、 + `devbase down` → `devbase up` でコンテナを作り直してください。 ### Fixed - **スナップショットの差分バックアップが、毎回ボリューム全体を控えていたのを直しました。** diff --git a/docs/specifications/base-image-rendering.md b/docs/specifications/base-image-rendering.md index df36bfc7..9e374df1 100644 --- a/docs/specifications/base-image-rendering.md +++ b/docs/specifications/base-image-rendering.md @@ -14,10 +14,13 @@ fontconfig は Chromium / Playwright のスクリーンショット、PDF の生 規則は `containers/base/fonts-local.conf` の 1 ファイルで表し、イメージの中では `/etc/fonts/local.conf` に置く。追加のパッケージを要さず、設定そのもののサイズは 0 である。 -ブラウザは Playwright の Chromium をイメージに持つ。置き場は `/opt/ms-playwright` -(`ENV PLAYWRIGHT_BROWSERS_PATH`)で、amd64 と arm64 のどちらでも、建て直した直後から -ネットワーク無しで起動してスクリーンショットや PDF を作れる。システムの Chrome(`google-chrome-stable`)と -Ubuntu の `chromium-browser`(snap スタブ)は、どちらのアーキにも入れない。 +base はブラウザを持たない。base にあるのは空のブラウザの置き場 `/opt/ms-playwright` +(`ENV PLAYWRIGHT_BROWSERS_PATH`)と npm のグローバルの `@playwright/test` だけである。 +Playwright の Chromium・ブラウザの依存パッケージ・追加の太さのフォント(`fonts-noto-cjk-extra`)は、 +ブラウザの派生イメージ `devbase-browser`(`containers/browser`)が持つ。このイメージを選ぶと、 +amd64 と arm64 のどちらでも、建て直した直後からネットワーク無しで起動してスクリーンショットや +PDF を作れる。システムの Chrome(`google-chrome-stable`)と Ubuntu の `chromium-browser` +(snap スタブ)は、どちらのイメージにも入れない。`terraform` も base に入れない。 利用者向けの読み方は [コンテナ操作ガイド: 文字の描画と、文書を扱う道具](../user/container-operations.md#文字の描画と文書を扱う道具base-以降) @@ -27,8 +30,9 @@ Ubuntu の `chromium-browser`(snap スタブ)は、どちらのアーキに - base イメージの fontconfig の設定の置き場所と内容、フォントキャッシュの作り直し - base イメージに同梱する、文書を扱う道具のパッケージ -- base イメージのブラウザ(Playwright の Chromium の置き場。システムの Chrome を入れないこと) -- base から派生するイメージ(`general` / `go` / `php` / `php85` / `bi-tools` / `latex` / +- ブラウザ(base の空の置き場と、Chromium・依存パッケージ・追加の太さのフォントを持つ + ブラウザの派生イメージ `devbase-browser`。システムの Chrome を入れないこと) +- base から派生するイメージ(`browser` / `general` / `go` / `php` / `php85` / `bi-tools` / `latex` / `trygroup`)への伝播の規則 - `ENV LANG` は設定しない。LibreOffice と `pip` は同梱しない @@ -42,7 +46,7 @@ Ubuntu の `chromium-browser`(snap スタブ)は、どちらのアーキに | 受け皿 | イメージに実在しない書体名を指定されたときに末尾へ足すフェイス | ブラウザの語(ブラウザの置き場・Playwright の Chromium・システムの Chrome・snap スタブ・ -ブラウザの依存パッケージ)の定義は +ブラウザの依存パッケージ・ブラウザの派生イメージ)と、追加の太さのフォント・派生イメージの定義は [用語集: ブラウザ(`browser`)](../glossary.md#ブラウザbrowser) にある。 ## 構成要素 @@ -51,14 +55,16 @@ Ubuntu の `chromium-browser`(snap スタブ)は、どちらのアーキに | --- | --- | --- | | フォントの規則 | `containers/base/fonts-local.conf` | 4 つの ``、未導入の書体の受け皿 1 つ、中国語・韓国語を明示した指定を守る 8 つの ``。先頭のコメントに置き場所を動かせない理由を持つ | | 規則の配置 | `containers/base/Dockerfile` の末尾の `COPY` 群 | `COPY --chmod=0644 fonts-local.conf /etc/fonts/local.conf`。直後の `RUN sudo fc-cache -f` | -| 道具のパッケージ | `containers/base/Dockerfile` の 1 つ目の `RUN` の 1 回目の `apt-get install` | metric 互換の 2 つと、文書を扱う 4 つ | +| 道具のパッケージ | `containers/base/Dockerfile` の 1 つ目の `RUN` の 1 回目の `apt-get install` | metric 互換の 2 つと、文書を扱う 4 つ。解決先を保つ `fonts-liberation`・`fonts-ipafont-gothic`・`fonts-wqy-zenhei` | | 形の検査 | `tests/containers/test_base_dockerfile_fonts.py` | Docker を起動せずに Dockerfile と `fonts-local.conf` の形を固定する | | 解決先の検査 | `tests/containers/test_base_image_font_matching.py` | 建てたイメージの中の `fc-match` の解決先を固定する | -| ブラウザの置き場の宣言 | `containers/base/Dockerfile` の `ENV PLAYWRIGHT_BROWSERS_PATH=/opt/ms-playwright` | 置き場の値を 1 か所で決め、以降の `RUN` と `docker exec` の非対話の処理へ届ける | -| ブラウザの置き場の作成 | `containers/base/Dockerfile` の npm のグローバル領域を作る root の `RUN` | `install -d -m 2775 -o "$USERNAME" -g npm "$PLAYWRIGHT_BROWSERS_PATH"` | -| Chromium の取得 | `containers/base/Dockerfile` の利用者の `RUN` | `npx playwright install --with-deps chromium` で依存パッケージを入れ、Chromium を置き場へ取得する。末尾の片付けは置き場を消さない | -| ブラウザの形の検査 | `tests/containers/test_base_dockerfile_playwright.py` | Docker を起動せずに、base の置き場の宣言・置き場の作り方・片付け・apt の一覧を固定する | -| ブラウザの検査 | `tests/containers/test_base_image_browser.py` | 建てたイメージで Chromium が起動し、日本語のページを PDF にできることを固定する | +| ブラウザの置き場の宣言 | `containers/base/Dockerfile` の `ENV PLAYWRIGHT_BROWSERS_PATH=/opt/ms-playwright` | 置き場の値を 1 か所で決め、以降の `RUN`・派生イメージ・`docker exec` の非対話の処理へ届ける | +| ブラウザの置き場の作成 | `containers/base/Dockerfile` の npm のグローバル領域を作る root の `RUN` | `install -d -m 2775 -o "$USERNAME" -g npm "$PLAYWRIGHT_BROWSERS_PATH"`。base では空のまま | +| ブラウザの派生イメージ | `containers/browser/Dockerfile`・`containers/browser/compose.yml` | `FROM devbase-base:latest` の上に `fonts-noto-cjk-extra` を apt で入れ、`npx playwright install --with-deps chromium` で依存パッケージと Chromium を置き場へ取得し、`fc-cache -f` でキャッシュを作り直す | +| base の形の検査 | `tests/containers/test_base_dockerfile_playwright.py` | Docker を起動せずに、base にブラウザ・追加の太さのフォント・`terraform` が無いこと、置き場の宣言と作り方を固定する | +| 派生イメージの形の検査 | `tests/containers/test_browser_dockerfile.py` | Docker を起動せずに、`containers/browser` の `FROM`・取得・片付け・`fc-cache` の順を固定する | +| base の検査 | `tests/containers/test_base_image_without_browser.py` | 建てた base に Chromium・追加の太さのフォント・`terraform` が無く、Chromium の起動が次にすることを示して止まることを固定する | +| ブラウザの検査 | `tests/containers/test_browser_image.py` | 建てたブラウザの派生イメージで Chromium が起動し、日本語のページを PDF にでき、追加の太さのフォントがあることを固定する | 型(クラス)は持たない。設定ファイルと Dockerfile の命令だけで構成する。 @@ -156,7 +162,7 @@ zh-cn / ko の `` は、**総称ファミリを名指ししたときだ `` が JP を先頭にするため、韓国語が日本語の字形になる。`ko` だけ書くと非対称で、なぜ `zh-cn` が無いのかが後から読めないため、総称ファミリ 4 つ × 言語 2 つ = 8 つを並べて対称にする。 -これらのフェイスは `fonts-noto-cjk` / `fonts-noto-cjk-extra` に既にあり、追加の導入を要さない。 +これらのフェイスは標準の太さで、base の `fonts-noto-cjk` に既にあり、追加の導入を要さない。 ### 解決先 @@ -202,11 +208,8 @@ zh-cn / ko の `` は、**総称ファミリを名指ししたときだ ### キャッシュの作り直し -`RUN sudo fc-cache -f` は、`COPY` の直後かつ**Playwright がフォントを入れる `RUN` より後**に -置く。その `RUN` の `npx playwright install --with-deps chromium` が `fonts-wqy-zenhei` / -`fonts-ipafont-gothic` / `fonts-liberation` などを入れ、直後に `~/.cache` を消す(Chromium 自体は -`PLAYWRIGHT_BROWSERS_PATH` の `/opt/ms-playwright` に置き、消さない)。前で走らせると、後から -入った書体を知らないキャッシュが残る。 +`RUN sudo fc-cache -f` は、`COPY` の直後に置く。フォントはすべて 1 つ目の `RUN` の apt で入り、 +利用者の `RUN` が末尾で `~/.cache` を消すため、その後でキャッシュを作り直す。 **`fc-cache` は設定を反映するためのものではない**(設定は照合のたびに読まれる)。走らせる のは、`~/.cache` を消した後に `/var/cache/fontconfig` を作り直し、コンテナの初回起動時の @@ -215,22 +218,34 @@ zh-cn / ko の `` は、**総称ファミリを名指ししたときだ 末尾へ置くことにはキャッシュの上の利点もある。`fonts-local.conf` を書き換えたとき、無効に なるのは末尾の数層だけで、巨大な 1 つ目の `RUN` は建て直されない。 +ブラウザの派生イメージは、追加の太さのフォントと Playwright の `--with-deps` が入れるフォントを +足した後に、自分の末尾でもう一度 `RUN sudo fc-cache -f` を打つ。 + ### ブラウザ -**Chromium は両アーキとも Playwright のものを使う。** base は Playwright の Chromium を -ブラウザの置き場 `/opt/ms-playwright` に持ち、`ENV PLAYWRIGHT_BROWSERS_PATH` で示す。Playwright の -既定の置き場 `~/.cache/ms-playwright` は利用者の `RUN` の末尾の片付け(`sudo rm -rf /tmp/* ~/.cache …`)で -消えるため、片付けの対象外の `/opt` に置く。`ENV` にするのは、`docker exec` の非対話の処理からも -同じ置き場を使うためである。 +**base はブラウザを持たない。** Playwright の Chromium・ブラウザの依存パッケージ・追加の太さの +フォントは、ブラウザの派生イメージ `devbase-browser`(`containers/browser`)が持つ。base は +空のブラウザの置き場と、npm のグローバルの `@playwright/test` だけを持つ。 -命令は次の順に並ぶ。 +base の命令は次の順に並ぶ。 1. `ENV PLAYWRIGHT_BROWSERS_PATH=/opt/ms-playwright`(`ENV NPM_CONFIG_PREFIX` / `ENV PATH` の後)。 1 つだけ宣言する -2. root の `RUN` が、npm のグローバル領域の隣で、同じ形で置き場を作る +2. root の `RUN` が、npm のグローバル領域の隣で、同じ形で空の置き場を作る (`install -d -m 2775 -o "$USERNAME" -g npm "$PLAYWRIGHT_BROWSERS_PATH"`) -3. 利用者の `RUN` が `npx playwright install --with-deps chromium` で、ブラウザの依存パッケージを - apt で入れ、Chromium を置き場へ取得する。同じ `RUN` の末尾の片付けの `rm -rf` は、 + +Playwright の既定の置き場 `~/.cache/ms-playwright` は利用者の `RUN` の末尾の片付け +(`sudo rm -rf /tmp/* ~/.cache …`)で消えるため、片付けの対象外の `/opt` に置く。`ENV` にするのは、 +`docker exec` の非対話の処理と、実行時にブラウザを取得する道具からも同じ置き場を使うためである。 +置き場を base に置くため、派生イメージは `ENV` と置き場を宣言し直さずに継ぐ。 + +ブラウザの派生イメージの命令は次の順に並ぶ。 + +1. `FROM devbase-base:latest`。プロジェクトの `devbase build` が base を先に建てる経路に乗る +2. root の `RUN` が `fonts-noto-cjk-extra` を apt で入れ、同じ `RUN` で `/var/lib/apt/lists` を消す +3. 利用者 `ubuntu` の `RUN` が `npx playwright install --with-deps chromium` で、ブラウザの依存 + パッケージを apt で入れ、Chromium を置き場へ取得する。`npx` は base の `@playwright/test` の + CLI を使う。同じ `RUN` の末尾の片付けの `rm -rf` は `/var/lib/apt/lists` を消し、 `/opt/ms-playwright` もその親の `/opt` も対象にしない 4. `RUN sudo fc-cache -f`(「キャッシュの作り直し」) @@ -240,24 +255,25 @@ zh-cn / ko の `` は、**総称ファミリを名指ししたときだ 所有者とパーミッションの決まりを 1 つにそろえる。 **取得の命令は `--with-deps chromium` で、headless の shell だけに絞らない。** 本体と headless の -shell の両方を置くと、本体を要する使い方(`channel: 'chromium'` の起動など)も arm64 で使える。`--with-deps` を残すため、ブラウザの -依存パッケージ(`fonts-liberation`・`fonts-ipafont-gothic`・`fonts-wqy-zenhei`・`libnss3` など)も -そのまま入る。 - -**システムの Chrome(`google-chrome-stable`)はどちらのアーキにも入れない。** Google の apt の -取得元も足さない。Chromium を使う手段は両アーキとも Playwright の Chromium で足り、システムの Chrome -(`/usr/bin/google-chrome`)を呼ぶ処理はリポジトリにもプラグインにも無いためである。amd64 だけに -入れると、amd64 の base が Chrome と依存(systemd・gtk3 など)で約 540MB 大きくなり、アーキで -道具もそろわない。Playwright を介さずにブラウザを呼ぶ道具には、Playwright の Chromium の実行ファイル -(`NODE_PATH="$(npm root -g)" node -e "console.log(require('@playwright/test').chromium.executablePath())"` で得るパス)を渡す。 +shell の両方を置くと、本体を要する使い方(`channel: 'chromium'` の起動など)も arm64 で使える。 -| 場面 | 振る舞い | -| --- | --- | -| イメージの Playwright を使う | `/opt/ms-playwright` の Chromium を起動する。取得は起きない | -| 道具が別の版の Playwright で `playwright install chromium` を打つ | `/opt/ms-playwright` へ別の版を足す。イメージの外の変更なので、コンテナを作り直すと消える | -| Playwright を介さずにブラウザを呼ぶ | システムの Chrome は無い。Playwright の Chromium の実行ファイルのパスを渡す | +**base で Chromium が無いことは、Playwright 自身の失敗の出力で伝える。** base で Playwright の +Chromium を起動すると、Playwright は置き場に実行ファイルが無いことと `npx playwright install` を +出して止まる。取得した後に依存パッケージが無ければ、`install-deps` を打てと出す。base に案内の +ための包みは置かない。 -base を `FROM` で継ぐ派生イメージには `ENV` と置き場がそのまま届く。 +**システムの Chrome(`google-chrome-stable`)はどちらのイメージにも入れない。** Google の apt の +取得元も足さない。Chromium を使う手段はブラウザの派生イメージの Playwright の Chromium で足りる。 +Playwright を介さずにブラウザを呼ぶ道具には、Playwright の Chromium の実行ファイル +(`NODE_PATH="$(npm root -g)" node -e "console.log(require('@playwright/test').chromium.executablePath())"` で得るパス)を渡す。 + +| 場面 | base 系(base・`general`・`php` など) | ブラウザの派生イメージ | +| --- | --- | --- | +| イメージの Playwright で Chromium を起動する | 失敗する。Playwright が `npx playwright install` を打てと出す | `/opt/ms-playwright` の Chromium を起動する。取得は起きない | +| 利用者が `npx playwright install chromium` を打つ | 置き場へ取得できるが、依存パッケージが無いため起動で失敗する。Playwright が `install-deps` を出す | 同じ版なら何も起きない | +| 利用者が `npx playwright install --with-deps chromium` を打つ | 起動できるようになる。イメージの外の変更なので、コンテナを作り直すと消える | — | +| 道具が自分の版で取得する(`@playwright/mcp`・`playwright-kit`) | 取得は済むが、依存パッケージが無いため起動で失敗する | 置き場へ自分の版を足して起動する。コンテナを作り直すと足した版は消える | +| Playwright を介さずにブラウザを呼ぶ | 呼べるブラウザが無い | システムの Chrome は無い。Playwright の Chromium の実行ファイルのパスを渡す | ### 入れないもの @@ -266,9 +282,12 @@ base を `FROM` で継ぐ派生イメージには `ENV` と置き場がそのま | LibreOffice | 展開 372〜459MB で、base の規律に見合わない | | `pip` / `pip3` | 既にある `uv` / `uvx` で賄う | | `ENV LANG` | 設定するとコンテナの中のすべてのコマンドの出力・ソート順・日付の書式が変わり、影響がフォントの外へ出る。総称ファミリの解決先そのものを日本語にすれば `lang` のヒントは要らない | +| Playwright の Chromium とブラウザの依存パッケージ(base) | すべての利用者は使わず、arm64 で計約 960MB ある。ブラウザの派生イメージが持つ | +| `fonts-noto-cjk-extra`(base) | 追加の太さ(Thin・Light・Medium・Black など)で、arm64 で約 215MB ある。標準の太さは `fonts-noto-cjk` にある。ブラウザの派生イメージが持つ | +| `terraform` と HashiCorp の apt の取得元 | 使う者が見当たらない。取得元を残すと、base を継ぐイメージの `apt-get update` が HashiCorp の配布元に頼る。要るプロジェクトは自分の Dockerfile かフックで入れる | | `chromium-browser` | Ubuntu の snap スタブで、コンテナの中では Chromium として起動しない。Chromium は Playwright のものを使う | -| システムの Chrome(`google-chrome-stable`) | Chromium は両アーキとも Playwright のもので足り、呼ぶ処理も無い。amd64 では依存と合わせて約 540MB ある | -| `fonts-wqy-zenhei` の削除 | 削除しても OS 既定の `65-nonlatin.conf` が `sans-serif` の prefer 一覧にこの書体を含むため日本語にはならず、中国語のページを豆腐にするだけになる。Dockerfile に導入の行は無く、Playwright が依存として入れる | +| システムの Chrome(`google-chrome-stable`) | Chromium は Playwright のもので足り、呼ぶ処理も無い。amd64 では依存と合わせて約 540MB ある | +| `fonts-wqy-zenhei` の削除 | 削除しても OS 既定の `65-nonlatin.conf` が `sans-serif` の prefer 一覧にこの書体を含むため日本語にはならず、中国語のページを豆腐にするだけになる。1 つ目の `RUN` の 1 回目の一覧で明示して入れる | Dockerfile は `fonts-wqy-zenhei` を対象とする `apt-get remove` / `apt-get purge` / `dpkg -r` を 持たない。 @@ -289,6 +308,14 @@ Dockerfile は `fonts-wqy-zenhei` を対象とする `apt-get remove` / `apt-get | `fonts-crosextra-carlito` | `Calibri` の metric 互換の実体 | | `fonts-crosextra-caladea` | `Cambria` の metric 互換の実体 | +「解決先」の表を保つため、次の 3 つも同じ 1 回目の一覧で明示して入れる。 + +| パッケージ | 解決先の表の行 | +| --- | --- | +| `fonts-liberation` | `Arial` / `Times New Roman` / `Courier New` → Liberation Sans / Serif / Mono | +| `fonts-ipafont-gothic` | `IPAPGothic`(名指し) | +| `fonts-wqy-zenhei` | `WenQuanYi Zen Hei`(名指し) | + metric 互換の 2 つは、`30-metric-aliases.conf` が既に持っている対応の**実体**である。実体が 無いと `Calibri` / `Cambria` の指定は行き先を失い、中国語のフォントへ落ちる。 @@ -300,7 +327,7 @@ metric 互換の 2 つは、`30-metric-aliases.conf` が既に持っている対 ## 運用 - 変更は**イメージを建て直すまで反映されない**。`devbase build base --no-cache` で base を - 建て直し、使っている派生イメージ(`general` / `go` / `php` / `php85` / `bi-tools` / `latex` / + 建て直し、使っている派生イメージ(`browser` / `general` / `go` / `php` / `php85` / `bi-tools` / `latex` / `trygroup`。いずれも `FROM devbase-base:latest`)も建て直し、稼働中のコンテナは `devbase down` → `devbase up` で作り直す。この 3 段はいずれも省けない - **`devbase rebuild` はここでは使えない。** `devbase build --expires=7` のシノニム @@ -311,9 +338,12 @@ metric 互換の 2 つは、`30-metric-aliases.conf` が既に持っている対 - LibreOffice での実際の描画は未検証である。LibreOffice は base に無く、fontconfig とは別の照合も 持つ。Chromium での描画は、建てたイメージの中で日本語のページを PDF にし、埋め込まれた書体を `pdffonts` で見る検査が確かめる(「テスト観点」) +- ブラウザ・追加の太さのフォントが要るプロジェクトは、dev サービスの `build.context` を + `${DEVBASE_ROOT}/containers/browser/` にして `devbase-browser` を選ぶ。移り方は + [コンテナ操作ガイド: ブラウザ・追加の太さのフォント・terraform を使う](../user/container-operations.md#ブラウザ追加の太さのフォントterraform-を使う) + にある - 実行時に道具がブラウザの置き場へ取得したブラウザは、コンテナを作り直すと消える - (`/home/ubuntu` もボリュームではないため、以前の `~/.cache/ms-playwright` と同じである)。 - イメージが持つ Chromium は消えない + (`/home/ubuntu` もボリュームではない)。ブラウザの派生イメージが持つ Chromium は消えない - 解決先の表は arm64 で採ったものである。apt で入れるパッケージは両アーキで同じだが、同じ表になるかは amd64 の端末で建てるまで分からない @@ -325,9 +355,9 @@ metric 互換の 2 つは、`30-metric-aliases.conf` が既に持っている対 `tests/containers/test_base_dockerfile_fonts.py`(Docker を要さない): -- 6 パッケージが 1 回目の `apt-get install` の一覧にあること。 +- 6 パッケージと、解決先を保つ 3 つのフォントが 1 回目の `apt-get install` の一覧にあること。 - `COPY` の宛先が `/etc/fonts/local.conf` であり、`conf.d/` を宛先にする `COPY` が無いこと。 -- `fc-cache -f` が 1 度だけで、`COPY` より後かつ Playwright の `RUN` より後にあること。 +- `fc-cache -f` が 1 度だけで、`COPY` より後にあること。 - `fonts-local.conf` が整形式の XML で、4 つの `` と 9 つの ``(受け皿 1 つと、 総称ファミリ 4 つ × 言語 2 つの 8 つ)を持つこと。`lang` の `` が `family` の `` を必ず持つこと。 @@ -358,19 +388,43 @@ CI はイメージを建てるジョブを持たないため、このテスト `tests/containers/test_base_dockerfile_playwright.py`(Docker を要さない): -- base に `ENV PLAYWRIGHT_BROWSERS_PATH=/opt/ms-playwright` が 1 つだけあり、 - `npx playwright install` を含む `RUN` より前にあること。 -- base の置き場を、npm のグローバル領域と同じ root の `RUN` で `$USERNAME:npm`・`2775` で作り、 - その `RUN` が `ENV` より後かつ `npx playwright install` より前にあること。 -- base の `npx playwright install` を含む `RUN` が 1 つで `--with-deps chromium` を持ち、その片付けの - `rm -rf` が `/opt`・`/opt/ms-playwright`・`$PLAYWRIGHT_BROWSERS_PATH` を対象にしないこと。 +- base のどの `RUN` にも `playwright install` が無く、どの `apt-get install` にも + `fonts-noto-cjk-extra`・`terraform` が無く、HashiCorp の apt の取得元が無いこと。 +- `npm i -g` の一覧に `@playwright/test` があること。 +- base に `ENV PLAYWRIGHT_BROWSERS_PATH=/opt/ms-playwright` が 1 つだけあり、置き場を + npm のグローバル領域と同じ root の `RUN` で `$USERNAME:npm`・`2775` で作り、その `RUN` が + `ENV` より後にあること。 - base のどの `apt-get install` にも `chromium-browser` が無いこと。 - Google の apt の取得元(`dl.google.com/linux`)が無く、どの `apt-get install` にも `google-chrome` が無いこと。 +- `containers/bi-tools/Dockerfile` の先頭のコメントが、base に含まれるものとして `terraform` を挙げないこと。 + +`tests/containers/test_browser_dockerfile.py`(Docker を要さない): + +- 最初の `FROM` が `devbase-base:latest` で、`compose.yml` の `image` が `devbase-browser:latest` であること。 +- `npx playwright install --with-deps chromium` を含む `RUN` が 1 つで利用者 `ubuntu` が打ち、 + その片付けの `rm -rf` が `/var/lib/apt/lists/*` を消し、`/opt`・`/opt/ms-playwright`・ + `$PLAYWRIGHT_BROWSERS_PATH` を対象にしないこと。`ENV PLAYWRIGHT_BROWSERS_PATH` を宣言し直さないこと。 +- `fonts-noto-cjk-extra` を apt で入れる `RUN` が 1 つあり、`fc-cache -f` が 1 度だけで、 + その `RUN` と Playwright の `RUN` より後にあること。 + +`tests/cli/test_build_browser_image.py`(Docker を要さない): + +- dev の `build.context` が `${DEVBASE_ROOT}/containers/browser/` のプロジェクトで `bin/devbase build` を + 起動すると、`devbase-base` を建ててから `docker compose build dev` を打つこと。 -`fc-cache -f` が Playwright の `RUN` より後にあることは -`tests/containers/test_base_dockerfile_fonts.py` が見る。 +`tests/containers/test_base_image_without_browser.py`(Docker を要する): -`tests/containers/test_base_image_browser.py`(Docker を要する): +- 置き場 `/opt/ms-playwright` の下に `chromium-*`・`chromium_headless_shell-*` が無いこと。 +- `dpkg -s fonts-noto-cjk-extra` と `command -v terraform` が非 0 で終わること。 +- `--network none` で起動したコンテナで、利用者 `ubuntu` の非対話の `bash -c` から Playwright の + Chromium を headless で起動すると、非 0 で終わり、出力に `playwright install` か `devbase-browser` が + 含まれること。 + +検査するイメージは環境変数 `DEVBASE_TEST_BASE_IMAGE` で差し替えられ、既定は `devbase-base:latest` +である。既定のイメージの置き場に Chromium があるとき(ブラウザを base から外す前に建てたイメージ)は +skip し、明示したときは skip せず落とす。 + +`tests/containers/test_browser_image.py`(Docker を要する): - 置き場 `/opt/ms-playwright` があること。 - `--network none` で起動したコンテナで、利用者 `ubuntu` の非対話の `bash -c` から Playwright の @@ -382,14 +436,16 @@ CI はイメージを建てるジョブを持たないため、このテスト `fonts-ipafont-gothic`・`fonts-wqy-zenhei`・`libnss3`)は 0 で終わること。 - `env` の `PLAYWRIGHT_BROWSERS_PATH` が `/opt/ms-playwright` で、Chromium を起動した後も `~/.cache/ms-playwright` が無いこと。 +- `dpkg -s fonts-noto-cjk-extra` が 0 で終わり、`fc-list` の `Noto Sans CJK JP` に `Thin` と `Black` の + フェイスがあること。 - `docker run` はセッションで 1 回に抑え、並列でも同じワーカーへ寄せる(`xdist_group`)。 -検査するイメージは環境変数 `DEVBASE_TEST_BASE_IMAGE` で差し替えられ、既定は `devbase-base:latest` -である。マージ前は `devbase-base:latest` を上書きしない別のタグを渡し、リリース後は既定のまま同じ +検査するイメージは環境変数 `DEVBASE_TEST_BROWSER_IMAGE` で差し替えられ、既定は `devbase-browser:latest` +である。マージ前は `devbase-browser:latest` を上書きしない別のタグを渡し、リリース後は既定のまま同じ 検査を走らせるためである。skip するのは、Docker が使えないとき、イメージが無いとき、既定の -イメージに置き場が無いとき(置き場より前に建てたイメージ)だけである。**`DEVBASE_TEST_BASE_IMAGE` を -明示したときは、置き場が無くても skip せず落とす。** 片付けで置き場を消して建てたイメージは古い -イメージと見分けられず、skip にすると壊れ方が見えなくなるためである。 +イメージに置き場が無いときだけである。**`DEVBASE_TEST_BROWSER_IMAGE` を明示したときは、置き場が +無くても skip せず落とす。** 片付けで置き場を消して建てたイメージは古いイメージと見分けられず、 +skip にすると壊れ方が見えなくなるためである。 ## 関連リンク diff --git a/docs/user/container-operations.md b/docs/user/container-operations.md index e024c060..7b3431e7 100644 --- a/docs/user/container-operations.md +++ b/docs/user/container-operations.md @@ -387,6 +387,7 @@ graph TD A[Ubuntu 26.04] --> B[base] B --> C[general] B --> G[go] + B --> J[browser] C --> D[php] C --> I[php85] C --> E[latex] @@ -404,11 +405,12 @@ graph TD | イメージ | ベース | 主な内容 | 用途 | |---------|-------|---------|------| | **base** | Ubuntu 26.04 | Docker CLI、Python 3、日本語フォント、PDF / OOXML の道具、shellcheck | 最小限の開発環境 | -| **general** | base | AWS CLI、gcloud、Terraform、Node.js 20、AI CLI | 汎用開発環境 | +| **general** | base | AWS CLI、gcloud、Node.js 20、AI CLI | 汎用開発環境 | | **php** | general | PHP 8.5、Composer、MySQL Shell | PHP 8.5 系 開発 | | **php85** | general | PHP 8.5、Composer、MySQL Shell | PHP 8.5 系 開発 | | **latex** | general | LaTeX | 文書作成 | | **go** | base | Go 開発環境 | Go 開発 | +| **browser** | base | Playwright の Chromium とその依存パッケージ、Noto CJK の追加の太さ(Thin〜Black) | ブラウザの操作、HTML の PDF・スクリーンショット | | **snapshot** | Ubuntu 26.04 | zstd のみ(約 80MB) | スナップショット専用 | ### 文字の描画と、文書を扱う道具(base 以降) @@ -417,7 +419,9 @@ base イメージは、文字を描くときの既定を**日本語**にして `sans` / `serif` / `monospace`)と、イメージに無い書体名(`Meiryo` / `Yu Gothic` / `MS PGothic` / `Noto Sans JP` など)は、いずれも Noto CJK の **JP** フェイスへ解決されます。 fontconfig は Chromium / Playwright のスクリーンショット、PDF の生成、画像の生成がすべて -参照するため、日本語を含むページを撮っても日本語の字形で写ります。 +参照するため、日本語を含むページを撮っても日本語の字形で写ります。base が持つ Noto CJK は +標準の太さ(Regular・Bold)です。追加の太さ(Thin・Light・Medium・Black など)は `browser` の +イメージにあります。 | 指定 | 解決先 | |------|--------| @@ -447,20 +451,15 @@ fontconfig は Chromium / Playwright のスクリーンショット、PDF の生 > **LibreOffice と `pip` は入っていません。** LibreOffice は展開 372〜459MB で base の規律に > 見合わないため入れていません。Python パッケージが要るときは `uv` / `uvx` を使ってください。 -ブラウザは Playwright の Chromium が amd64 と arm64 の両方に入っています。置き場は -`/opt/ms-playwright`(環境変数 `PLAYWRIGHT_BROWSERS_PATH`)で、ネットワーク無しで起動できます。 - -> **システムの Chrome(`google-chrome-stable`、`/usr/bin/google-chrome`)は入っていません。** -> Playwright から使うときは何も変えずに Chromium が起動します。Playwright を介さずにブラウザの -> 実行ファイルを渡す道具には、次で得られる Playwright の Chromium のパスを渡してください。 -> -> ```bash -> NODE_PATH="$(npm root -g)" node -e "console.log(require('@playwright/test').chromium.executablePath())" -> ``` +base にはブラウザが入っていません。Playwright(`@playwright/test`)は入っていますが、 +Chromium とその依存パッケージは `browser` のイメージにあります。base 系のイメージ(base・ +`general`・`php` など)で Playwright の Chromium を起動すると、`npx playwright install` を +打つよう示して止まります。使い方は次の節を見てください。 > **これらは `devbase build base --no-cache` で base を建て直すと反映されます。** -> `devbase up` だけでは反映されません。派生イメージ(`general` など)を使っている -> プロジェクトは、その派生イメージも建て直してください。 +> `devbase up` だけでは反映されません。派生イメージ(`general`・`browser` など)を使っている +> プロジェクトは、その派生イメージも建て直し、`devbase down` → `devbase up` でコンテナを +> 作り直してください。 > **`devbase rebuild` では建て直りません。** `devbase build --expires=7` のシノニムのため、 > 期限内はビルドそのものを飛ばします。 @@ -468,6 +467,68 @@ fontconfig は Chromium / Playwright のスクリーンショット、PDF の生 [base イメージの文字の描画と、文書を扱う道具](../specifications/base-image-rendering.md) にあります。 +### ブラウザ・追加の太さのフォント・terraform を使う + +base は、すべての利用者が使う道具だけを持ちます。次のものは base にありません。 + +| 要るもの | 使い方 | +|----------|--------| +| ブラウザ(Playwright の Chromium とその依存パッケージ) | `browser` のイメージを選ぶ | +| Noto CJK の追加の太さ(`fonts-noto-cjk-extra`) | `browser` のイメージを選ぶ | +| `terraform` | プロジェクトの Dockerfile かフックで入れる | + +**`browser` のイメージを選ぶ。** プロジェクトの `compose.yml` の dev サービスで、`build.context` を +`containers/browser` にします。ほかの派生イメージと同じく、`devbase build` は base を先に建ててから +このイメージを建てます。 + +```yaml +services: + dev: + build: + context: ${DEVBASE_ROOT}/containers/browser/ +``` + +建て直した直後から、ネットワーク無しで Chromium を起動し、日本語のページを PDF や +スクリーンショットにできます。置き場は `/opt/ms-playwright`(環境変数 `PLAYWRIGHT_BROWSERS_PATH`)です。 + +> **システムの Chrome(`google-chrome-stable`、`/usr/bin/google-chrome`)はどのイメージにも入っていません。** +> Playwright から使うときは何も変えずに Chromium が起動します。Playwright を介さずにブラウザの +> 実行ファイルを渡す道具には、`browser` のイメージで次のコマンドが出す Playwright の Chromium の +> パスを渡してください。 +> +> ```bash +> NODE_PATH="$(npm root -g)" node -e "console.log(require('@playwright/test').chromium.executablePath())" +> ``` + +**`php` などほかの派生イメージの道具も要るとき。** `browser` のイメージには PHP などの道具が +ありません。次の道筋から、プロジェクトの道具立てに合うものを選びます。 + +| 道筋 | すること | 気をつけること | +|------|----------|----------------| +| `browser` のイメージを選ぶ | 上の `build.context` に替える | 元の派生イメージの道具が無くなる | +| プロジェクトの Dockerfile で足す | `FROM devbase-php:latest` などの上で、利用者 `ubuntu` が `npx playwright install --with-deps chromium` を打つ | `devbase build` は `devbase-php` を建てるが、その下の base が無いときに先に建てない。先に `devbase build base` を打つ | +| 道具の起動引数でブラウザの場所を渡す | MCP の起動引数などで、上の 2 つのどちらかで用意した Chromium の実行ファイルを指させる | ブラウザと依存パッケージは、ほかの 2 つの道筋で用意する | + +`@playwright/mcp` や `chrome-devtools-mcp` は、既定でシステムの Chrome を探すことがあります。 +選んだ道筋で、実際に起動できることを確かめてください。 + +**コンテナの中で一時的に取得する。** base 系のコンテナでも、次のコマンドで Chromium と依存 +パッケージを取得できます。コンテナを作り直すと消え、取得のたびに数百 MB をネットワークから取ります。 + +```bash +npx playwright install --with-deps chromium +``` + +**`terraform` を入れる。** プロジェクトの Dockerfile(root の `RUN`)か、コンテナ起動時のフックで +HashiCorp の apt の取得元を足して入れます。 + +```bash +sudo install -m 0755 -d /etc/apt/keyrings +curl -fsSL https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/hashicorp-archive-keyring.gpg +echo "deb [signed-by=/etc/apt/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list +sudo apt-get update && sudo apt-get install -y terraform +``` + ### Bash の静的検査(base 以降) base イメージには [ShellCheck](https://www.shellcheck.net/)(`shellcheck`)が入っています。 From 9bb85fe17a8b2bada9288434a90ef26d55fc48fd Mon Sep 17 00:00:00 2001 From: "takemi.ohama" Date: Sat, 3 Oct 2026 13:02:28 +0900 Subject: [PATCH 4/4] =?UTF-8?q?docs(design):=20=E5=8F=97=E3=81=91=E5=85=A5?= =?UTF-8?q?=E3=82=8C=E6=9D=A1=E4=BB=B6=207=20=E3=82=92=E5=B1=95=E9=96=8B?= =?UTF-8?q?=E5=BE=8C=E3=81=AE=E3=83=95=E3=82=A1=E3=82=A4=E3=83=AB=E3=81=AE?= =?UTF-8?q?=E5=A4=A7=E3=81=8D=E3=81=95=E3=81=A7=E5=88=A4=E5=AE=9A=E3=81=99?= =?UTF-8?q?=E3=82=8B=EF=BC=88#402=20=E3=81=AE=E6=B1=BA=E5=AE=9A=2010?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01MJKpaMqwf3k95XaRpRG26N --- issues/issue-402-design.md | 10 ++++++++-- issues/issue-402-requirements.md | 4 ++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/issues/issue-402-design.md b/issues/issue-402-design.md index a611d131..d14fbf3b 100644 --- a/issues/issue-402-design.md +++ b/issues/issue-402-design.md @@ -318,7 +318,7 @@ sequenceDiagram | 大項目 | 要求の条件 | 実現方式 | 確かめ方 | | --- | --- | --- | --- | -| 性能・拡張性 | base が arm64 で 1.2GB 以上減る。ブラウザの派生イメージの大きさを変更前の base と比べて記録する | 外す 4 つは base の層から命令ごと消す。3 つのフォントだけを残す。ブラウザの派生イメージは今の base の取得と同じ命令を足すだけにし、層を増やしすぎない | 同じ arm64 の端末で、変更前の base・変更後の base・ブラウザの派生イメージを作業用のタグで建て、`docker image inspect --format '{{.Size}}'` の値と差を PR に残す。建てる前に `docker system df` で空きを見る。1.2GB に届かなければフォントを外さず人へ戻す(前提 3) | +| 性能・拡張性 | base が arm64 で 1.2GB 以上減る。ブラウザの派生イメージの大きさを変更前の base と比べて記録する | 外す 4 つは base の層から命令ごと消す。3 つのフォントだけを残す。ブラウザの派生イメージは今の base の取得と同じ命令を足すだけにし、層を増やしすぎない | 同じ arm64 の端末で、変更前の base・変更後の base・ブラウザの派生イメージを作業用のタグで建て、展開後のファイルの大きさ(`du -sxb /`)と `.Size`(圧縮後)の値と差を PR に残す。建てる前に `docker system df` で空きを見る。1.2GB に届かなければフォントを外さず人へ戻す(前提 3) | | 移行性 | 移る作業は `compose.yml` の 1 か所か Dockerfile の追記で済む。ボリュームとデータに触れない | イメージの選択だけで移れるよう、ブラウザの派生イメージを `containers/` に置き、`build.context: ${DEVBASE_ROOT}/containers/browser/` で選べるようにする。置き場の値は base と同じにする | 利用者向けの文書の移り方の節を、`proj-a` の Pull Request の差分と突き合わせる | | 運用・保守性 | 建て直しの順(base → 派生イメージ → `devbase down` / `up`)を文書と CHANGELOG に書く | 確定仕様の「運用」と利用者向けの文書の注記に、3 段とも省けないことを書く。`devbase rebuild` では建て直らないことも書く | PR で文書の記述を `grep` した出力を残す(#220 の決定 4 と同じ) | | システム環境 | 受け入れ条件は arm64 で確かめ、amd64 はリリース後テスト | アーキで分ける命令を新しく足さない。Playwright の CLI がアーキに合う Chromium を選ぶ | arm64 は手元で建てて検査を通す。amd64 は #242 と同じくリリース後テストで同じ検査を通す | @@ -415,6 +415,12 @@ base を先に建てるのは、プロジェクトの dev サービスの Docker 根拠: Value 1 / P1(MVV 版 1) +### 決定 10: 依頼の 1.2GB と同じ物差しで測るため、受け入れ条件 7 は展開後のファイルの大きさで判定する + +実装で測ると、containerd のイメージストアの `docker image inspect` の `.Size` は圧縮後の大きさを返した。差は 611,133,492 バイトで、展開後のファイル(`du -sxb /`)の差は 1,291,660,733 バイトだった。依頼の表の大きさ(Chromium 662MB など)と課題の本文の「展開後の大きさが 1.2GB 以上減る」は展開後の物差しであり、受け入れ条件 7 が `.Size` を指したのは測り方の取り違えだった。前提 3 のとおり人へ戻し、利用者が展開後の大きさで判定すると決めた(2026-10-03)。`.Size` の値も PR に並べて残す。取得の量(圧縮後)の減りは約 611MB である。 + +根拠: 課題の本文の受け入れ条件 1 / 前提 3 + ## テスト設計 | 受け入れ条件・不変条件 | どの振る舞いで縛るか | どう壊したら落ちるべきか | @@ -427,7 +433,7 @@ base を先に建てるのは、プロジェクトの dev サービスの Docker | 受け入れ条件 4・I6 | 建てた変更後の base で `test_base_image_font_matching.py` が `EXPECTED_MATCHES` を書き換えずに通る | 3 つのフォントの 1 つを外して建てると、`Arial` / `WenQuanYi Zen Hei` / `IPAPGothic` のどれかの行が落ちる | | 受け入れ条件 5・I8 | `--network none` の変更後の base で、利用者 `ubuntu` の非対話の `bash -c` から Chromium を起動すると非 0 で、出力に `playwright install` か `devbase-browser` が含まれる | base に Chromium を戻して建てると、起動が 0 で終わって落ちる | | 受け入れ条件 6・I7 | 変更後の base で `dpkg -s fonts-noto-cjk-extra` と `command -v terraform` が非 0、置き場に `chromium-*`・`chromium_headless_shell-*` が無い | どれかを戻して建てると、その項目が落ちる | -| 受け入れ条件 7 | 変更前と変更後の base の `.Size` の差が 1,200,000,000 以上(手動。PR に値を残す) | — | +| 受け入れ条件 7 | 変更前と変更後の base の展開後のファイルの大きさ(`du -sxb /`)の差が 1,200,000,000 以上(手動。`.Size` の値とともに PR に残す) | — | | 受け入れ条件 8・I12 | 移したテストが、`DEVBASE_TEST_BROWSER_IMAGE`(既定 `devbase-browser:latest`)のイメージで #220 の受け入れ条件 5〜9 を確かめる。明示したイメージに置き場の中身が無ければ skip せず落とす | ブラウザの派生イメージの片付けに置き場を足して建てると、PDF の作成と置き場の項目が落ちる | | 受け入れ条件 9・I13 | 建てたブラウザの派生イメージで `dpkg -s fonts-noto-cjk-extra` が 0、`fc-list` に `Noto Sans CJK JP` の `Thin` と `Black` がある | 派生イメージの apt から `fonts-noto-cjk-extra` を外して建てると落ちる | | 受け入れ条件 10・I9 | ブラウザの派生イメージの最初の `FROM` が `devbase-base:latest`。dev の `build.context` が `containers/browser` のプロジェクトで `bin/devbase build` を起動すると、`devbase-base` を建ててから `docker compose build dev` を打つ | `FROM` を別の名前にすると、形の検査と経路の検査の両方が落ちる | diff --git a/issues/issue-402-requirements.md b/issues/issue-402-requirements.md index fb0a957c..91bbb64d 100644 --- a/issues/issue-402-requirements.md +++ b/issues/issue-402-requirements.md @@ -73,7 +73,7 @@ - 前提 6: 派生イメージ lfm は #403 で先に廃止されている(F4)。この変更は lfm を扱わない - 前提 7: ブラウザを使うプロジェクトの洗い出しは、手元にある 3 つのプラグインのリポジトリと `projects/*/repo` に限る。手元に無いプロジェクトは、CHANGELOG と利用者向けの文書の移り方で知らせる - 前提 8: 派生イメージの名前・数(ブラウザと文書のフォントを 1 つにまとめるか分けるか)・base に `ENV PLAYWRIGHT_BROWSERS_PATH` とブラウザの置き場を残すか・HashiCorp の apt の取得元を残すかは設計で決める。受け入れ条件はどの切り方でも判定できる形で書く -- 前提 9: 大きさは同じ arm64 の端末で、変更前の base と変更後の base を `docker image inspect --format '{{.Size}}'` で測り、差を PR に記録する。base を 2 本並べるため、建てる前に `docker system df` で空きを見て、作業用のタグは記録の後に消す(`AGENTS.md` の落とし穴) +- 前提 9: 大きさは同じ arm64 の端末で、変更前の base と変更後の base を、展開後のファイルの大きさ(コンテナで `du -sxb /`)で測り、差を PR に記録する。`docker image inspect --format '{{.Size}}'`(containerd のイメージストアでは圧縮後の大きさ)の値も並べて残す。base を 2 本並べるため、建てる前に `docker system df` で空きを見て、作業用のタグは記録の後に消す(`AGENTS.md` の落とし穴) ## 対象範囲 @@ -140,7 +140,7 @@ Dockerfile の形(Docker を起動しないテストで確かめる): - [ ] 4. `tests/containers/test_base_image_font_matching.py` が、`EXPECTED_MATCHES` を書き換えずに変更後の base で通る(標準の太さの日本語・中国語・韓国語と欧文の metric 互換の解決先が変わらない) - [ ] 5. 変更後の base を `--network none` で起動し、利用者 `ubuntu` の非対話の `bash -c` で Playwright の Chromium を headless で起動しようとすると、終了コードが非 0 で、出力にブラウザの派生イメージの名前か取得のコマンド(`playwright install`)が含まれる - [ ] 6. 変更後の base で `dpkg -s fonts-noto-cjk-extra` が非 0、`command -v terraform` が非 0 で終わり、`/opt/ms-playwright` の下に Chromium の版のディレクトリ(`chromium-*`・`chromium_headless_shell-*`)が無い -- [ ] 7. 変更前の base と変更後の base の `docker image inspect --format '{{.Size}}'` の差が、arm64 で 1.2GB(1,200,000,000 バイト)以上ある。両方の値と差を PR に記録する(前提 3) +- [ ] 7. 変更前の base と変更後の base の展開後のファイルの大きさ(`du -sxb /`)の差が、arm64 で 1.2GB(1,200,000,000 バイト)以上ある。両方の値と差を、`.Size`(圧縮後)の値と差とともに PR に記録する(前提 3・前提 9) ブラウザの派生イメージ(arm64):