本当は怖いHPC

HPC屋の趣味&実益ブログ

DockerfileのARGとFROMの順序と変数のスコープ

Dockerfileの中のFROMARGコマンドの位置関係について、ちょっとハマることがあったのでまとめておきます。

このあたりは、英語でも日本語でも直接的にはっきり書いてあるドキュメントがほとんどないので調べるのに時間がかかりました。

なお、実験は MacOS上の Docker version 19.03.8, build afacb8b で行っています

問題

まず、以下の例を見てみます。

# Dockerfile
ARG UBUNTU_VER
FROM ubuntu:${UBUNTU_VER}
RUN echo UBUNTU_VER=$UBUNTU_VER
$ docker build . --build-arg UBUNTU_VER=18.04 --no-cache
Sending build context to Docker daemon  2.048kB
Step 1/3 : ARG UBUNTU_VER
Step 2/3 : FROM ubuntu:${UBUNTU_VER}
 ---> 4e5021d210f6
Step 3/3 : RUN echo UBUNTU_VER=$UBUNTU_VER
 ---> Running in b1db86ff5314
UBUNTU_VER=
Removing intermediate container b1db86ff5314
 ---> 212d057cc765
Successfully built 212d057cc765

冒頭に ARG で定義した UBUNTU_VER という変数を、コマンドラインから --build-arg を使って代入しています。ここで、FROM の行では正しく値が設定されて ubuntu:18.04 というイメージが使われているのに対し、3行目の RUN コマンドの行では値が代入されていません。

どういうことでしょうか? ARG は、FROMより前に書いてはいけないのでしょうか?しかし、FROMコマンドで変数を使うためには、FROMより前にARGを書く必要があります。そもそも、FROMでは変数を使うことができているのに、その後のRUNでは空(未定義)なのはなぜでしょうか?

理由の説明

これには理由があります。まず、 FROM コマンドは、新しいステージ(build stage)を作成し、その次の行から スコープ を形成します。普通のプログラムで言う変数のスコープと同じです。また FROM より前に出現する ARG は、グローバルスコープ(globally-scoped ARG)となります。同様に、普通の関数でいうグローバル変数のようなものです。

次に、グローバルスコープの ARG は、それぞれの FROM スコープへは自動的には引き継がれません。スコープ毎に ARG コマンドを使って宣言する必要があります。また、その際にデフォルト値を変更することもできます。

ということで、それを実験するために以下のようなDockerfileをビルドしてみます。

# Dockerfile

# グローバルスコープのARG
ARG VER=18.04

FROM ubuntu:${VER} as base1
# (1) ローカルスコープARG宣言無し
RUN echo 1 VER=${VER}

FROM ubuntu:${VER} as base2
# (2) ローカルスコープのARG宣言(デフォルト値なし)
ARG VER
RUN echo 2 VER=${VER}

FROM ubuntu:${VER} as base3
# (3) ローカルスコープのARG宣言(デフォルト値あり)
ARG VER="In 3rd FROM"
RUN echo 3 VER=${VER}

FROM ubuntu:${VER} as base4
# (4) ローカルスコープのARG宣言(再びデフォルト値なし)
ARG VER
RUN echo 4 VER=${VER}

このDockerfileをビルドすると、下のようになります。(不要な部分を省略済)

Sending build context to Docker daemon  3.584kB
Step 1/12 : ARG VER=18.04
Step 2/12 : FROM ubuntu:${VER} as base1       # <-- (0)
 ---> 4e5021d210f6
Step 3/12 : RUN echo 1 VER=${VER}
 ---> Running in a954660d91fd
1 VER=                                        # <-- (1)

Step 6/12 : RUN echo 2 VER=${VER}
 ---> Running in a8ac09e221dc
2 VER=18.04                                   # <-- (2)

Step 9/12 : RUN echo 3 VER=${VER}
 ---> Running in 79466895adae
3 VER=In 3rd FROM                            # <-- (3)

Step 12/12 : RUN echo 4 VER=${VER}
 ---> Running in 5ebfe419e434
4 VER=18.04                                  # <-- (4)

Successfully built c32c9a07c503

ここで起こっていることを整理してみます。

  • まず、Dockerfileの冒頭でグローバルスコープの VER を定義します。これにより、 FROM コマンドで変数 VER が使えるようになります(出力の (0))。以降のすべての FROM コマンドで VER が使えます。
  • 次に、FROMで作成されたスコープの中で変数宣言なしで VER を使ってみると、中身は空です(出力の (1))。
  • 新しく作った別のスコープで、デフォルト値無しで VER を宣言します。すると、VER 変数が有効になり、値はグローバルスコープの定義を引き継ぎます(出力の (2))。
  • さらに新しく作った別のスコープでデフォルト値付きで変数宣言をすると、VER は宣言された値に設定されます(出力の `(3))。
  • また新しくスコープを作り、再び「デフォルト値なしのARG宣言」をしてみると、値はグローバルスコープのものになります。つまり、直前のスコープで設定した値は破棄されます。

まとめ

Dockerfileの中で FROM コマンドは新しいスコープを作り、その中で宣言された ARG は、そのローカルスコープの中のみで有効です。最初の FROM より前に宣言した ARG はグローバルスコープとなり、 FROMコマンド自体の中で使うことができますが、ローカルスコープの中では継承されません。ローカルスコープの中で変数を使いたい場合は、グローバルスコープの宣言があったとしても、再度 ARG で宣言する必要があります。また、ローカルスコープで設定した値は、他のローカルスコープには引き継がれません。

参考文献

C++ conceptsを試してみた (1)

C++20 から導入されるConceptsを試してみました。

cpp_akira さんがConceptの解説を書かれている[1]ので、自分なりに咀嚼して試してみます。

ビルド方法

現時点(2019年10月)で、Conceptを利用したプログラムをビルドするには、g++9以降が必要です。gccとclangのC++機能実装状況 (gcc[2], clang[3])によれば、

  • gccは実装済みPRが提案されている
  • clangは未対応

のようです。MacのHomebrewでインストールされたg++はパッチ適用済みのようですので、これを使います。

実際に、Mac上でConceptを使ったビルドをしてみます。 -std=c++2a および -fconcepts のフラグが必要です。

$ g++ -std=c++2a -fconcepts main.cpp

Conceptを使って型の制約を記述する

テンプレート引数の型がメンバ関数を持っている

// https://wandbox.org/permlink/njPaUxLdeDuXa81N
// コンセプト Drawable を定義
template <class T>
concept Drawable = requires(T& x) {
  x.draw();
};

// テンプレート引数のクラスが特定のメンバ関数を持っていることを規定する
template <Drawable T>
void f(T& x) {
  x.draw();
}

ただし、現時点では <concepts> ヘッダのincludeを含め、すべての機能が動くわけではないようです。

例:

// [1]より引用
// https://wandbox.org/permlink/G1SAoPNBVIwPYvVk


#include <concepts> 
template <class T, class U>
concept EqualityComparable = requires (T a, U b) {
  {a == b} -> std::convertible_to<bool>; // 式の戻り値型も制約できる (直接の戻り値型は指定できない)
};

引き続き、詳細な文法等の調査をしていこうと思います。

参考文献/リンク

変更履歴

  • 誤字修正 thanks to @takeda25 さん

目次のないPDFに目次を追加する

目的と概要

自炊したPDFには目次が含まれないため、読むときに不便です。この点、Kindleの書籍などとは雲泥の差があります。そこで、自分で目次を付与するスクリプトを作成しました。MacもしくはLinuxのコマンドラインツールgsを使い、そのツールへの入力をPythonで生成します。目次のデータ自体は、YAMLフォーマットで自分で用意します。

実装

gs コマンドを使うと、PDFに目次を付与することができます。 具体的には、規定のフォーマット(慣習として pdfmarks と呼ばれる)を用意し、それを目的のPDFとともに gs コマンドに渡すことで実現できるようです。

入力する pdfmarks のフォーマットは、詳細は仕様書にありますが、概要としては以下のようにすれば良いようです。こちらのページから引用

[/Count 3 /Title (Chapter 1) /Page 1 /OUT pdfmark
[/Count -2 /Title (Section 1.1) /Page 2 /OUT pdfmark
[/Title (Section 1.1.1) /Page 3 /OUT pdfmark
[/Title (Section 1.1.2) /Page 4 /OUT pdfmark
[/Count -1 /Title (Section 1.2) /Page 5 /OUT pdfmark
[/Title (Section 1.2.1) /Page 6 /OUT pdfmark
[/Title (Section 1.3) /Page 7 /OUT pdfmark

これを手で書くのは骨が折れますので、現代的なフォーマットから生成できるように、スクリプトを作成します。ここではYAMLファイルを入力とします。具体的なデータ構造は適当に決めました。

コード

実際のスクリプトはここにあります。

Generate pdfmarks (TOC) file, to feed to gs command · GitHub

入力例

# Table of Contents from 「明解演習 数理統計 (明解演習シリーズ)」 by 小寺 平治
# https://www.kyoritsu-pub.co.jp/bookdetail/9784320013810

base_page: 9
toc: 
  - title: "第1章 確率"
    page: 1
    sub:
      - {"title": "順列 組み合わせ", "page": 1}
      - {"title": "確率とその声質", "page": 2}
      - {"title": "事象の独立性", "page": 5}
  - title: "第2章 確率変数"
    page: 26
    sub:
      - {"title": "2.1 確率変数 確率分布", "page": 26}
      - {"title": "2.2 多次元分布", "page": 27}
      - {"title": "2.3 確率変数の関数の分布", "page": 28}
      - {"title": "2.4 平均 分散", "page": 29}
      - {"title": "2.5 積率母関数", "page": 31}

このようなデータを用意し(手で作成)、スクリプトに食わせます。

$ python pdfmark.py --infile=pdfmarks.yaml --outfile=pdfmarks 
$ gs -dBATCH -dNOPAUSE -sDEVICE=pdfwrite -sOutputFile=out.pdf in.pdf pdfmarks

日本語への対応

日本語へ対応するには、一工夫必要でした。PDFにおいてはマルチバイト文字などの文字列は PDFEncoding と呼ばれる方法によってエンコードされているので、これに従って文字列をエンコードする必要があります。これは、 pdfrw というライブラリを用いることで簡単に実現できました。

今後

目次情報は手動で作成しなければいけないので、今後はこれを自動で取得できるようにしたいと思います。

参考文献

【広告】