maybe daily dev notes

私の開発日誌

Hardhat/Truffleを使わずにSolidityをコンパイルする

Hardhat/Truffle を使わずに、Solidityのスマートコントラクトをコンパイルしたい。そういった望みを持つこともあると思う。 そんなときに使える方法を紹介する。

solc-js

solc-js という solc (Solidityのコンパイラ) をJavaScriptからプログラマブルに呼び出せるようラップしたライブラリがある。 今回はこれを使ってSolidityのスマートコントラクトをコンパイルする。

github.com

インストール

solc-jsのインストールはnpmだと次のコマンドでできる:

npm i solc

なお、solc-jsには現状TypeScript用の型定義ファイルが同梱されていない。Type Definition for solc-js ? · Issue #578 · ethereum/solc-js · GitHub

このため、このままでは TypeScriptの記法で import することはできない。これを回避するにはいくつかの方法がある (以下を参照) ので、都合の良いものを採用しよう。

stackoverflow.com

以下では、JSの記法 const solc = require('solc') でインポートするようにする。将来solc-jsのTS対応が完了すれば型定義も同梱されるようなので、その時に直せば良いだろう。

使い方

ここまでできたら、次のようなコードでSolidityをコンパイルできる。

import * as fs from 'fs';
const solc = require('solc');

const compile = async (fileName: string, contractName: string) => {
  const file = fs.readFileSync(fileName).toString();
  const output = 'input.sol';
  const input = {
    language: 'Solidity',
    sources: {
      [output]: {
        content: file,
      },
    },
    settings: {
      optimizer: {
        enabled: true,
        runs: 200,
      },
      outputSelection: {
        [output]: {
          '*': ['evm.bytecode.object', 'abi', 'evm.deployedBytecode'],
        },
      },
    },
  };

  const out = JSON.parse(solc.compile(JSON.stringify(input), { import: findImports }));
  const contract = out.contracts[output][contractName];
  return { bytecode: `0x${contract.evm.bytecode.object}`, abi: contract.abi };
};

const importCache: { [key: string]: { contents: string } } = {};
const findImports = (path: string) => {
  if (importCache[path] == null) {
    const file = fs.readFileSync(`node_modules/${path}`);
    importCache[path] = {
      contents: file.toString(),
    };
  }
  return importCache[path];
};

// コンパイル実行
compile("sample.sol", "DemoContract");

基本的には solc.compile という関数に solc のStandard input jsonを渡せば良い。 上記コードでは input 変数がそれに当たり、ここを上記ドキュメントに沿って変更することで、任意の構成でコンパイルを実行できる。

ただし、実世界のSmart contractは何らかのライブラリに依存することが多い。その場合は、それぞれのimportを解決する必要がある。

importの解決のために、 { import: findImports } というオプションで解決の関数を渡す。findImports 関数は独自に実装する必要があるが、依存するファイルをローカルに用意しておけば、単にファイルを読み込んで返すだけで済む。

例えば、以下のようなimportを解決したい場合、

import "@openzeppelin/contracts/access/Ownable.sol";

事前に npm install @openzeppelin/contracts を実行すれば、 ./node_modules/@openzeppelin/contracts/contracts/access/Ownable.sol を読み込むだけでimport先のファイルを取得できる。

上記コードの findImports 関数でも、そのような処理を実行している。

コンパイルするまで依存するライブラリを予測できない場合は、findImportsの中でネットワーク越しに依存ライブラリを取得するような処理が必要になる。(けど、そんなユースケースは少なそう)

これだけでコンパイルができた。なお上のコードでは、最後にBytecodeとABIを返すようにしているが、これも standard output json を参照すれば任意の項目を返すようにカスタマイズできる。

上記のコードはまとめて以下のリポジトリに配置した。

github.com

その他

コンパイラのバージョンは以下で取得可能。基本的に solc-js 自体のバージョンと同じになる模様。

const solc = require('solc');
const version = solc.version();  // '0.8.13+commit.abaa5c0e.Emscripten.clang'

もしコンパイラのバージョンを変えたいときは、こちら: https://github.com/ethereum/solc-js#using-a-legacy-version

まとめ

Solidityのコンパイルくらいであれば、重厚なライブラリを使わずとも比較的簡単に実現できる。 ユースケースによってはこれが活きることもあるので、活用されたい。

Dockerfileが複数のBuild contextをサポートするようになった

はてな記事執筆機能の素振りがてら書いてみる。

序論

最近Dockerfileをビルドする際に複数のBuild contextを指定できるようになったらしい。

www.docker.com

これを使うと、例えば共通モジュールが別ディレクトリに切り出されているような場合に、よりきれいにビルドコンテキストを渡すことができる。 以下に簡単な例とともに紹介する。

従来の問題

例えばこんなディレクトリ構成の場合:

.
├── app1
│   ├── Dockerfile
│   └── index.py
├── app2
│   ├── Dockerfile
│   └── index.py
└── common
    └── util.py

従来だと app1 をビルドする際に common ディレクトリもBuild contextに含めたい場合、ルートディレクトリをBuild contextに含める必要があった。

# ビルド実行
docker build .

これだとDockerfile内でファイルを参照する際もルートディレクトリからの相対パスとなり、美しくない。また、Build contextに関係のないディレクトリ (app2) が含まれるのも(実害はなさそうだが)気になる。

FROM python:3.9.12-buster
COPY app1/index.py ./
COPY common/util.py ./
CMD ["python3", "index.py"]

この課題感はこちらのIssueでも議論されていた。 How to include files outside of Docker's build context? - Stack Overflow

新しいMulti build context機能

一方、新しいMulti build context機能を使うと、以下のように書ける。

# syntax=docker/dockerfile:1.4
FROM python:3.9.12-buster
# app1からの相対パスで解決
COPY index.py ./
# commonディレクトリは別のBuild contextから読み込み
COPY --from=common util.py ./
CMD ["python3", "index.py"]
# ビルド実行
cd app1
docker buildx build . --build-context common=../common

このように、buildx build コマンドの --build-context オプションにより、任意の数のBuild contextを名前付きで追加できる。これらのBuild contextは、上の例のように COPY --from などで指定可能 (Multi-stage buildで使う記法に似ている。)

これにより、単一Build contextしか渡せなかった従来の問題が解決されている。

その他

今のところ --build-context オプションは buildx build コマンドでのみ利用でき、 docker build コマンドでは利用できない。

docker buildx build | Docker Documentation

docker build | Docker Documentation

このため、既存ツールと連携したい場合は工夫が必要/不可能かもしれない。

自分は最近Docker buildをもっぱらAWS CDKから呼び出しているので、CDKが対応しない限りは使えなさそう。いずれはそういったところも対応してくれたらなーと思う。

はじめる

個人ブログを始めることにした
前はqiitaでやってたが、あちらの規約上許される内容なのか判断できないこともままあって、やりづらいから

はてなが最適なのかも判断しかねてるがとりあえず始めるべしという心意気

おそらく更新は低頻度になるが、なんとか続けていきたい



と書きつつ、手癖でテーマを開発日誌に設定してしまった……
実際はソフト開発以外のことも書きたいし、日誌というほどの頻度にはならないと思われる