前口上 (心の準備)
本課題の目標
この課題では,
- 世の中で実際に使われている,
- 大きなソフトウェアを改良,機能拡張できるようになることを目標にする.本課題のテーマは,実際のソフトウェア作りの現場 --- 製品づくりでも研究でも--- でほぼ必ず遭遇する場面:
- 自分で書いたわけではないので全容を把握していない,または,
- 全容を把握できない程度に大きい ソフトウェアを改良したり,機能拡張する場面を経験して,大きなソフトウェアを作るのに必要なスキルや考え方を身につけることである.
この課題を作った理由
ほとんどの大学のプログラミング課題は,あるトピック --- アルゴリズム,ネットワーク,音声,グラフィクス,... --- に関連するプログラムを「一から」作るという課題である.限られた時間内に,一から作るという制約上,作れるソフトウェアの規模は自ずと小さくなる.そのような課題は,プログラミング言語の基本を身につけ,対象となるトピックの基本を理解するためには必須・有用なものであるが,それを繰り返していても本格的な大規模プログラムを構築するために当たりまえのように使われている道具,スキル,知識,概念,etc. は身につかない.それらは,体系的に整理することが難しい,ないし,整理するほどのことでもないと思われているということもあり,あまり明示的に教えられることもない.この課題では,それを学ぶ --- 教えるというよりも自ら学んでもらう --- ことを目的とする.
ロードマップ
そのためにこの課題では,既存のオープンソースのソフトウェアの改良・機能拡張を目標とする.具体的にどのソフトウェアを対象とするか,それにどのような改良・拡張を施すかも自分達で決める.課題期間中の活動内容は,
-
デバッガを使ってソフトウェアの動きを追跡する練習
-
チーム作り
-
チームで,どのようなオープンソースソフトウェアを対象に,どのような改良・拡張を行うかを議論 する
-
その計画を早いうちに発表し,他のチームや教員・TA からのフィードバックを得る
-
定期的に進捗を発表し,他のチーム,教員・TA からのフィードバックを得る
-
活動内容,調査内容のドキュメントを残す (レポート) 最終的に達成した課題の有用さや難易度も大事だが,それをしっかりとドキュメント化 (あとに引き続く人が有用だと感じるドキュメントを残す) することも重要視する. この教科書の残りは主に上記の 1. を補足するために書かれているが,それは課題の内容の,最も表面的な部分に過ぎない.実際に身につけて欲しいのは,以下のことである.それは研究や製品作りの場面できっと有用になることである.
-
よくわからないソフトを解析して,なんとかする根性
-
背景理論もよく知らないソフトの背景理論を独習して,なんとかする根性
-
ツールの高度な使い方を,人から教わるのではなく,自分で一次ソース (と言ってもソフトの使い方の一次ソースはマニュアルであり,歴史的文書と異なり,簡単に手に入る) を調べて,自らスキルを高めていく根性
本サイト
- 本サイトでは, オープンソースをダウンロードして, ビルド (特に, デバッガで追跡できるようにするためのビルド), デバッガで追跡する手順を, 代表的な言語・エコシステムについて説明する
- C/C++
- Python
- JavaScript (Node.js)
- Rust
- Go
- ただし一部は書きかけ
- 2024年度までのサイトでは C/C++ の場合について詳細解説しているが, 本サイトでは Python, JavaScript など近年よく使われている (実験でも選ばれている) エコシステムの説明を充実させ, 説明の多くの部分を動画でカバーした
(任意) 開発用仮想マシン環境の構築
- 開発のために仮想マシンを使って, その中にLinux (Ubuntu) 環境を構築しそこで開発をすることを推奨する
- 理由:
- 開発やビルドのために必要なツール, ライブラリなどをインストールするにあたり自分の環境を汚さずに済む
- 多くのソフトはLinux上で開発されており, 環境に依存したトラブルに時間を消費する可能性が低い
- 上記の系として, チームの全員が同じ環境 (OSとそのバージョン) を使って開発ができる
- 具体的なやり方は, オペレーティングシステムの講義でも使っている 仮想マシン, Linuxのインストール のページを参照
- ただしそれなりに時間 (やり方を知っている人がトラブル無しで20分; 仮想マシン自体が初めてならおそらく一時間以上) がかかるので, 授業時間中にやるのがいいかどうかはわからない
- あとでやるか, 他の作業と並行してやるのがよい
- 仮想マシンは, GUIアプリケーションを開発する場合はデスクトップをインストール, そうでなければサーバーで良い
- このページを参照して, ホスト (PC本体) からゲスト (仮想マシン) へ SSH でログインできるようにし, SSHで作業をすると良い (仮想マシンのコンソールは使いづらい)
- GUIアプリケーションを開発する場合は, 直接仮想マシンのデスクトップで操作をする
ソースコードのダウンロード
tarball
- "tarball"は多数のファイルを収めたディレクトリ一式を, tarというコマンドで一つのファイルにまとめ (Windowsでzipに馴染みのある人はそれのUnix版だと思えば良い), 通常はそれを gzip, bzip2 などの圧縮ファイルで圧縮したものである.
- そのためtarballのファイル名は通常, xyz.tar.gz (gzipの場合) または xyz.tar.gz2 (bzip2の場合)
- その昔は, ソースコードの配布はビルドに必要なファイルをすべて収めたディレクトリ --- ソースツリー --- をtarballの形式にしてダウンロードささせるだけ, というパターンが多かった
- tarball が無事入手できたら, 以下のコマンドで解凍するとディレクトリ構造が復元される
$ tar xf xyz.tar.bz2
- 慣習上, 通常はこれで
xyzという名前の, 全てのファイルを含んだディレクトリができるが必ずそうなるとは限らない (あくまでtarballを作った人の名前付けに依存する) し, 場合によっては複数のファイルを解凍した場所にぶちまける行儀の悪いtarballもある - したがって安全のため, 作業は空のディレクトリを作って行うのがよい
$ mkdir empty
$ # empty/ の中に xyz.tar.bz2 を入れる
$ cd empty
$ tar xvf xyz.tar.bz2
xvfのx... 解凍 (extract)v... 進捗表示 (verbose) : ファイルが解凍されるたびにその名前を表示するf... ファイル指定 : xyz.tar.bz2 が解凍の対象であることを指定
練習
- GNU awk というプログラムの tarball をダウンロードしてみよ
- 空フォルダを作りそこでそれを解凍してみよ
- 検索で見つけることを推奨するが見つからないときのため, 一応 ここ
github
- C/C++に限らず殆どの言語で, 多くのオープンソースプロジェクトのソースコードが github から提供されている
- ソフトの名前がわかっていればGoogleで "ソフト名 github" などとすれば github 上の, そのソフトの着地ページが見つかることだろう
- github上で, あるプロジェクトのソースコードを収めた実体をそのプロジェクトの「レポジトリ」と呼ぶ
- そのレポジトリを開発用マシンに, コピー (clone) することでソースコードを含んだディレクトリがまるごとダウンロードできる
- 着地ページが見つかったら, 同ページの Code という緑のボタンをおすとレポジトリをcloneするための URL もしくはコマンドが表示される
- URLは
git@github.com:xyz/xyz.git
もしくは
https://github.com/xyz/xyz.git
のような文字列
- これをcloneするコマンドは
git clone レポジトリのURL
$ git clone git@github.com:xyz/xyz.git
もしくは
$ git clone https://github.com/xyz/xyz.git
git コマンドが見つからなければ, Ubuntuであれば以下のようにしてインストールする
$ sudo apt install git
練習
- Python のライブラリである
artの git レポジトリを発見し,git cloneしてみよ- 検索のヒント: python art github
- 見つからなければ ここ
開発・デバッグ用のビルド
- tarballダウンロードにせよ, git cloneにせよ, 結果的にはソースコードが一式収められたディレクトリが手に入る
- 次のステップはそれをビルドすることである
- その際, 開発・修正用にビルド, インストールするため, 以下に注意する
- インストール先のディレクトリを意識 (指定) する --- 特に, 普段自分が使っているコマンドなどが入っているのとは異なる場所にインストールする
- デバッガで追跡できるようにする --- これは主に, 事前にコンパイルする言語で注意が必要. Python, JavaScriptなど直接実行する言語では意識する必要がない場合が多い
- 具体的には
- デバッグ用オプション (
-g3) をつけてコンパイルする --- 大雑把には各命令がソースコードのどの場所 (ファイル名や行番号) から生成されたか, 各変数がどこに格納されているか, などを記録した情報 --- デバッグシンボル --- を生成する. これがないとデバッガが現在の実行位置をソースコード上の位置として表示することができなくなる. - (必須ではないが) 最適化をしないオプション(
-O0) を指定する --- 最適化をすると様々なコードの再配置が行われ, 実際の命令とソースコードの対応がわかりにくくなる. デバッガ上で実行していると, ソースコードの順番と異なる順番で実行されたり, 変数の値が表示できなかったりするなど,分析がしにくくなる
- デバッグ用オプション (
C/C++ + autotools/cmake 編
- 主にCやC++で書かれたソースコードのビルド方法
- C, C++は昔から開発言語の主流であり, 歴史も深い
- そのためビルドの慣習もいくつかあり, 古い方式と新しい方式が混在している
- ビルドするためにはそのソフト用のビルド手順を読むのが基本である
- ... と述べた上で, ほとんどのソフトが従っている, よくあるパターンをいくつか説明しておく
configure; make; make install
基本
- tarballで入手したソースツリーには多くの場合,
configureという名前のファイル (実体はシェルスクリプト) が含まれる - そのソフトを 本番用ディレクトリ (
/usr/localなど)にインストールする場合 (今はこちらはやらない) の よくある手順は以下
# 注: 以下は本番ディレクトリにインストールするとき; 今はこちらは使わない
$ cd ソースツリーのトップディレクトリ
$ ./configure
$ make
$ sudo make install
./configureは- その環境で無事ビルドが可能かをチェックする --- 例えばある関数を使うのであればそれが使えるか (環境に必要なライブラリがインストールされているか) をチェックする
config.hを, (config.h.inを元に) 生成する . ここにはconfigureで判明した環境固有の情報 --- この関数が使えるとか使えないとか --- が書き込まれており, 多くのファイルから#include "config.h"で取り込まれるMakefileを (Makefile.inを元に) 生成する. 次のmakeコマンドで使われる
makeはMakefileに書かれた命令を読み込んで実際のビルド (コンパイルやリンク) を行う. 普通はここで,gcc,g++などのコマンドが実行されるmake installは最終的な生成物 --- 実行可能ファイル (いわゆるコマンド) やライブラリ --- を所定の場所 (コマンドは/usr/local/bin, ライブラリは/usr/local/libが典型) にコピーする. それらのディレクトリに書き込み権限がないといけないのでここだけsudo(root権限つき) で実行するのが普通
開発・デバッグ用の configure
ここで我々が行う開発・デバッグ用の configureでは
- インストール先ディレクトリの指定
- デバッグオプションの指定
- 最適化なしオプションの指定 を行うので, 我々がよく使うテンプレートは以下である
# 開発・デバッグ用のビルド・インストール
$ cd ソースツリーのトップディレクトリ
$ mkdir inst # インストール先ディレクトリを作る
$ ./configure --prefix=ソースツリーのトップディレクトリ/inst CFLAGS="-O0 -g3" CXXFLAGS="-O0 -g3"
$ make
$ make install
- ポイントは
configureに与えた以下のオプション--prefix=dir : インストール先のディレクトリをdirにするCFLAGS="-O0 -g3": Cコンパイラ (gccやclang) を起動するときのオプションを指定するCXXFLAGS="-O0 -g3": C++コンパイラ (g++やclang++) を起動するときのオプションを指定する
--prefixで指定したディレクトリなどは, 生成されるMakefileやconfig.hの中に焼き込まれ,makeコマンド, ひいては生成されるプログラムの動作に影響を与える- 注意:
configureスクリプトはソフトごとに異なる- すべてのソフトに共通した
configureという唯一無二のコマンドがあるわけではない - これは
configureの目的 --- そのソフトをビルドするのに必要なものが環境に備わっているかをチェックし, 環境の違いをconfig.hやMakefileに書き込む --- を考えれば当然
- すべてのソフトに共通した
- 一方で
configure自身, ツールで自動生成されるものであり, ほとんどのソフトに共通の動作がある--prefixなどのオプションはほぼ全てのconfigureが受け付けているCFLAGS=, CXXFLAGS=なども概ね受け付けている. が, 必ずそうだと信じ込まないほうが良い (確かめ方は以下)
- 自分が手にした
configureが何をサポートしているかは,
$ ./configure --help
で確かめられる
make
./configure後にmakeコマンドを実行するとgcc,g++などが (普通は数多く) 実行され, うまくすれば成功するmakeコマンドは一般的にはどのようなコマンドが実行されるかを表示してくれるので,CFLAGSやCXXFLAGSで指定したオプションが渡っているかに注目せよ- しかし, コマンドラインを垂れ流すのが汚いと思うからなのか, それを隠す
Makefileもある - そのような場合にはそれを露出させるオプションがないかを探ることに価値がある
- 典型的には
$ make VERBOSE=1
$ make V=1
make install
--prefixで指定したディレクトリ下に最終生成物をインストールする--prefix=D としたのであれば D/bin, D/lib, などのディレクトリができ, コマンドは D/binの下に生成されているはず
練習
- GNU awk のtarballをダウンロードし, configure; make; make install でビルド, インストールせよ
--prefixの指定CFLAGSの指定 を忘れずに, gccのコマンドラインにCFLAGSが渡っていることを目視せよ
autotools
- 上記で tarball には
configureが含まれておりその場合は述べた手順でビルドができる (ことが多い) と述べた - 一方
git cloneしてできたディレクトリはさにあらずで,configureを生成するというステップから行う想定である場合が多い - それは,
configure自身が非常にややこしいスクリプトであって, 手動で書くものではなく, 別のファイル (configure.ac) からツール (autoconf) を使って生成しており, git のレポジトリは通常, ツールで生成されたものは含まないよう管理されているという方針による - 実は tarball に同梱されているファイルには
configure以外にもそのような, ツールで生成されているもの (Makefile.inなど) があり, 生成方法もややこしい - 処世術としては,
configureが含まれておらず代わりにconfigure.acが含まれていたら,- 以下のコマンドでもろもろのファイルを生成する
$ autoreconf -i
configure, Makefile.inが生成されていることを確認
autorefonfを初めて実行した人は多くの場合, エラーになるだろう- 多くの場合以下の3つのツールをインストールすれば解決する
- autoconf
- automake
- libtoolize
$ sudo apt install autoconf automake libtoolize
-
その後の手順は前節 (
configure; make; make install) で述べたものと同じ -
configure.ac, Makefile.am, config.h.in
-
autoreconf -i-> configure, Makefile.in, libtool -
./configure-> Makefile config.h
CMake
基本
-
configure(autotools) があまりにもややこしいからか, それに変わるものとして CMake があり, 最近はそれが多い印象 -
makeと似た名前なのでmakeの代替のように見えるがそれは誤解で, あくまでconfigureのステップ (Makefileを生成するまで) の代替であり, 実際のコンパイルはmakeで行う -
CMakeでもconfigureでもどちらでもビルドできるようになっているプロジェクトもある
-
CMake でのビルドを想定しているソースツリーでは, (普通はトップレベルに)
CMakeLists.txtというファイルが存在している -
最低限, configureとの以下のような対応を覚えれば足りる
| configure | CMake |
|---|---|
--prefix=D | -DCMAKE_INSTALL_PREFIX=D |
CFLAGS="-O0 -g3" | -DCMAKE_C_FLAGS="-O0 -g3" |
CXXFLAGS="-O0 -g3" | -DCMAKE_CXX_FLAGS="-O0 -g3" |
-
cmakeには-DCMAKE_BUILD_TYPE=Debugというオプションを指定するだけで, 普通は "-g" フラグを指定してくれるが,"-O0 -g3"を指定したければ信用しないほうが良い (きちんと gcc などのコマンドラインを目視する) -
CMake は慣例として, 「生成物でソースツリーを汚さない (ソースと生成物は別ディレクトリ)」という方針が貫かれており, build途中のファイルが全て入ったディレクトリ (e.g.,
build) を作ってそこでcmakeもmakeも実行するのが普通である -
なお
cmakeが生成するMakefileはコマンドラインを表示しない (まるでそうすることが美しいと思っているかのような, 進捗表示だけを行う) が,make VERBOSE=1とすることで表示してくれるので開発をしたいならそれを推奨する -
以上より, 開発・デバッグ用の典型的なビルド手順は以下
$ cd ソースツリーのトップディレクトリ
$ mkdir build inst # 生成物置き場(build), 最終青果物置き場(inst) を作成
$ cd build
$ cmake -DCMAKE_INSTALL_PREFIX=ソースツリーのトップディレクトリ/inst -DCMAKE_C_FLAGS="-O0 -g3" -DCMAKE_CXX_FLAGS="-O0 -g3" ..
$ make VERBOSE=1
$ make install
-
注: cmake の最後の引数 (
..) はCMakeLists.txtの存在するディレクトリを指定している -
cmake では一般に,
-D変数=値という形でオプションを指定するが, おびただしいオプションがあり名前も長い上, どのようなオプションがあるのかがわかりにくい -
cmakeを実行すると変数の設定が
CMakeCache.txtというファイルから読み込まれるとともに, 施した設定が同じファイルに書かれるので, 以下のようなやり方が実践的である
$ cmake ..
$ # CMakeCahce.txt をエディタで変更する (CMAKE_C_FLAGS, CMAKE_CXX_FLAGS, CMAKE_INSTALL_PREFIX ほか)
$ cmake .. # もう一度cmake .. を実行
$ make VERBOSE=1
$ make install
練習
- CMakeでビルドすることを想定した小さなプロジェクトとして fastfetch を開発・デバッグ用にビルド, インストールして実行してみよ
- 適切にオプション (
CMAKE_C_FLAGS,CMAKE_CXX_FLAGS,CMAKE_INSTALL_PREFIX) を設定すること make VERBOSE=1をつけてコンパイラのコマンドラインを目視し,"-O0 -g3"が行き渡っていることを確認すること
コードを修正した後の再ビルド
- ソースコードの一部を修正してもう一度ビルドする場合は
makeとmake installをやり直せば良い
$ make
$ make install
- つまり
./configureやcmakeの再実行は通常は不要 makeはビルドを完了するのに必要なコマンド (修正があったファイルやそれに影響を受けるファイルのコンパイル) だけを実行してくれるので初回のビルドに比べて普通は高速に終わる- しかし人生においては, 時として全てをやり直したくなるときもある
- その場合は
make cleanとする
$ make clean
$ make
$ make install
configure の再実行
./configureを実行するとconfig.cacheというファイルが生成される- 以降の実行ではこれを参照して一部のコマンド実行をスキップする
- このせいで環境を修正したにもかかわらずエラーがで続けることがあるので, 万全を期したければ
config.cacheを消す
$ rm config.cache
$ ./configure
- もっとも
./configureからやり直す, その際に万全を期したいのであれば tarball の展開からやり直すのが一番確実
cmake の再実行
cmakeを実行するとCMakeCache.txtというファイルが生成されるcmake実行中にエラーがでて, 環境を修正したにもかかわらずエラーがで続ける場合,CMakeCache.txtを消してから再実行する
$ rm CMakeCache.txt
$ cmake ..
- この場合も, 初期状態に戻すことに万全を期したければ, ソースツリーごと一旦消去してやり直すのも一考する
configure/cmake時のエラー対策
./configureやcmake時にはエラーや警告が出ることがある- その環境ではビルドができない, ということを早めに検出して, その環境の何が問題なのかを指摘することが主な役割である
- 典型的なエラー
- ツール (コマンド) がない. 例えば gcc/g++ などのC/C++コンパイラがない
- ライブラリがない. 典型的にはそのライブラリがあれば存在しているはずのヘッダファイル (
xxxx.h) や共有ライブラリファイル (libxxx.so) がない
- それらの多くは必要なソフトをパッケージ管理ソフト (Ubuntuであれば
apt) でインストールすることで解決する. 例えばgccであれば,
$ sudo apt install gcc
- 入れるべきパッケージ名を調べる完璧な方法はない (OS (Linux? Mac? BSD?) が違ったり, 同じOSでもディストリビューション (Ubuntu? Arch? Fedora?) が異なればそれによってもパッケージ名が異なる可能性があるためある程度致し方ない)
make時のエラー (コンパイルエラー) 対策
configure/cmakeは成功したがmakeの最中にエラーがおきたらそれはじっくりと腰を据えた調査が必要になる- しっかりと,
- どのディレクトリで実行したどのコマンドがエラーを起こしているのか (だからそれを表示させることが重要!)を見極める
- 同じエラーを手動で再現する
- その上でエラーメッセージやその場所を見てじっくり原因を探る のが重要
- エラーの理由についてはありとあらゆる可能性があるとしか言えないが, おそらく他の環境 (OS, ディストリビューションやそのバージョン) ではコンパイル成功したものだから, 非常に初歩的なエラーということは稀
- 依存しているライブラリが更新されてインタフェースが変更された (ソースコードは少し前のバージョンを前提に書かれている), みたいなことが典型である
必要なパッケージ名を探り当てるいくつかの方法
./configure/cmake時によく遭遇するエラーは, 環境に必要なソフト (コマンド, ライブラリ) が入っていないことに起因するものが多い- 典型的な調査・解決方法
コマンドがないと言われた場合
- コマンド名=パッケージ名となっていることは結構多い
$ g++
g++: command not found
$ sudo apt install g++
- Ubuntuのデスクトップ環境では, シェルにコマンドを打ち込んでそのコマンドがない場合, 必要なパッケージ名を教えてくれる
$ g++
Command 'g++' not found, but can be installed with:
sudo apt install g++
- 教えてくれない場合,
command-not-foundというパッケージをインストールすると教えてくれるようになる
$ sudo apt install command-not-found
$ sudo apt update
- このあと改めて再ログインが必要になるかもしれない
ライブラリ (必要な関数が入ったパッケージ) がないと言われた場合
- Ubuntuのライブラリのパッケージ名は
libXXX-devという名前のパッケージ名になっていることが多い - 例えばライブラリ
gmpがないと言われたら,
$ sudo apt install libgmp-dev
をやってみる価値はある. ただしやや当てずっぽうがすぎるという嫌いはある
汎用的なパッケージ検索方法1 apt sesarch
- Ubuntu であれば
apt search キーワードでそのキーワードがパッケージ名や記述に含まれるパッケージを列挙してくれるので, あるパッケージ名が存在することやその簡単な説明を確認できるため,apt installを試みる前に試すことを推奨する
$ apt search libgmp-dev
$ apt search libgmp-dev | less # ページめくりで表示
$ apt search libgmp-dev | grep -A1 -B1 arithmetic # arithmeticが出現する行とその前後1行を表示
汎用的なパッケージ検索方法2 apt-file sesarch
- Ubuntu には
apt-fileというコマンドがありファイル名を与えるとそれが含まれるパッケージ名を教えてくれる
$ apt-file search gmp.h
$ apt-file search /gmp.h # igmp.h などが引っかかってほしくない場合
apt-fileをインストール, セットアップする手順は以下
$ sudo apt apt-file
$ sudo apt-file update
Python : venv + pip 編
基本
- Pythonで書かれたソフトウェアは pip というソフトウェア (Ubuntu の
aptの Python版だと思えば良い) でインストールできることが多い - ただし最近の
pipは全ユーザ共通のディレクトリ (/usr/localなど) へのインストールを非推奨 (最近は禁止に近い) としており, 個人のディレクトリへ, それも venv (仮想環境. 仮想マシンと紛らわしいが, つまりは目的に応じて複数の Python 環境を持てる仕組み) というものを組み合わせて指定した環境へインストールすることを前提にしている - Pythonプログラムは通常「ビルド」という手順は不要で,
pipもソースコード (.py) をインストールするので原理的にはpipでソースコードを入手可能だが, 通常は git でダウンロードする - git でソースコードをダウンロードした場合も
pipを使ってそのソースコードを適切な場所に配置するため結局pipが必要となる - そこで以下では venv, pip, ビルド手順, の順に説明する
- 動画で概要を見たい人用 (倍速推奨)
venv : virtual env
- Pythonのvenv は, 目的に応じた複数の Python 環境 (ここでは環境 = インストールされているパッケージの集合のことだと思えば良い) を持つことができる仕組み
- 最初に, システム共通の
pythonコマンド (python3という名前の場合もある) が使える状態を前提とする
$ which python
/usr/bin/python # システム共通の python
- ここで以下
$ python -m venv my_env
を実行すると,
my_envというフォルダが作られ, まっさらなPython環境がその下に作られる. 続けて- ここで以下
$ . my_env/bin/activate
を実行すると, my_env下のPython環境が使われるようになる
- 具体的には以下のように,
my_env下のpythonコマンドやpipコマンドが使われるようになる
$ which python
/ ... /my_env/bin/python # 絶対パス名
$ which pip
/ ... /my_env/bin/pip # 絶対パス名
- 仕組みは単純で,
PATHという環境変数で,my_env/binディレクトリが先に検索されるようにしているだけである
$ echo $PATH
- この
pipコマンドはパッケージをmy_env下にインストールするし,pythonコマンドはimport文を実行する際にmy_env下でモジュールを探す my_envディレクトリを消せばそのPython仮想環境は跡形もなく消える
余談: venvディレクトリの場所と名前
- venv でディレクトリ (上記の
my_env) を作る際にどこになんという名前で作るのが良いのだろう? 世の中では主に2つの流儀があるようである- プロジェクトに特化したディレクトリを, ソースツリー内に決まった名前 (e.g.,
.venv)で作る (venvという名前は避けておくのが無難だろう) - ホームディレクトリ直下に
~/.venvsのようなディレクトリを作りその下に集める なおどちらの場合も,.から始めることが世の中の慣習になっているようだが, 積極的な理由があるのか不明
- プロジェクトに特化したディレクトリを, ソースツリー内に決まった名前 (e.g.,
pip : Pythonのパッケージインストーラ
- pip であるPythonパッケージを見つけたら
pip install パッケージ名
でインストール可能
- ファイルは実行した
pipコマンドと同じPython環境内に配置される
パッケージの検索
- かつては
pip search 文字列というコマンドで検索できていたがあるときからその機能が廃止され, https://pypi.org/ から探せということになった (理解不能)
ビルド
- 通常はそのプロジェクトの github レポジトリが存在しているので git clone する (やり方はソースコードのダウンロード を参照
- ソースツリーのトップレベルに
pyproject.toml,setup.py,setup.cfgのどれかが見つかったら
$ pip install -e .
とすれば, ビルド(と言っても Pythonであればコンパイルは不要)及び, ファイルの配置が行われる
- なお,
-e(editable) オプションをつけると, ファイルのコピーは行われず, ソースツリー中のファイルがそのまま参照される- 本来インストールされる場所からソースツリー中にシンボリックリンク・ショートカットのようなものができるとイメージすればよい
- ソースコードを修正すればそれがそのまま反映されるので, 開発するのに便利
練習
- Pythonのライブラリ
artのgithubレポジトリを見つけ, - コードをダウンロード (git clone),
pip install -eで開発用インストールを行ってみよ
JavaScript (Node.js) : nvm + npm 編
概要
- JavaScript はもともとWebページの一部として書いて, 動的なページを作るためのプログラミング言語だった
- したがって実行環境はWebブラウザの中と決まっていたのだが, ブラウザ外でも通常のプログラミング言語として実行できるよう, 単体の処理系として切り出されたのが Node.js
- つまり Node.js = 普通の (ブラウザと無関係な) JavaScript の実行環境
- Python プログラムを実行するためにPython処理系 (
pythonコマンド)があるのと同じように JavaScript に対して Node.js (nodeコマンド) がある Node.jsというとまるでそれ自身がJavaScriptのプログラムのようだがそうではない (このネーミングは理解不能)
- Python プログラムを実行するためにPython処理系 (
- JavaScript で書かれたソフトウェアは npm (Node Package Manager) という仕組みで配布・インストールされることが多い
- つまりPython にとっての pip が JavaScript にとってはnpm
- Node.js は
nodeのバージョンによって動作が変わることが多いため, 複数のnodeのバージョンを切り替えられる nvm (Node Version Manager) を組み合わせて使うのが一般的 - そこで以下では nvm, npm, ビルド手順, の順に説明する
nvmをインストール
nvmコマンド自身は公式レポジトリ の指示にしたがって最新版を入れる- 2025/09/30 時点での指示は以下
$ curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash
- 実行すると以下のように, シェルのスタートアップファイル(
~/.bashrc)を「勝手に設定を書き加えたからね」と報告して勝手に書き換えるので一応確認しておく
$ curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 16631 100 16631 0 0 291k 0 --:--:-- --:--:-- --:--:-- 300k
... <省略> ...
=> Appending nvm source string to /home/tau/.bashrc
=> Appending bash_completion source string to /home/tau/.bashrc
=> Close and reopen your terminal to start using nvm or run the following to use it now:
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" # This loads nvm
[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion" # This loads nvm bash_completion
- 注:
~/.bashrcはログイン時に自動的に実行されるものではない (対話的シェルを起動したときに実行される)- bash を使っている場合, ログインしたときに自動的に実行されるようにするには同じ内容を,
~/.bash_profileにも書かなければならない - bash以外のシェル (zsh, fishなど) を使っている人はメッセージを見ながら適切に設定してください
- 問題なければ一度ログアウトしてログインし直す
nvmコマンドが実行されるかを確認
$ nvm --help
Node.js (node, npm) のインストールと切り替え
node/npm のインストール = nvm install
nvmコマンドがインストールできたらnvm install ...でnode/npmをインストールできる
$ nvm install node
とすると最新版のnode, npmがインストールされる
- 特定バージョン(例: 20)をインストールしたければ,
$ nvm install 20
のようにする
- インストール先は
~/.nvmの下, バージョンごとのディレクトリ - 確認
$ which node
/home/tau/.nvm/versions/node/v24.9.0/bin/node
$ which npm
/home/tau/.nvm/versions/node/v24.9.0/bin/npm
node/npm の切り替え = nvm use
nvmで複数のバージョンのnode/npmをインストールしたらnvm use ...で, 利用するnode/npmのバージョンを切り替えられる
$ nvm use node
は最新版を使う指定
- バージョンを指定したい場合は
$ nvm use 20
など
node,npmが起動できることと, バージョンを確認
$ node -v
v24.9.0
$ npm -v
11.6.0
npm 概論
- npm は, node パッケージを管理するツールであり, パッケージをインストールするには
$ npm install パッケージ名
とする
- これで, 指定したパッケージ + それが依存する他のパッケージ もインストールされる
- ファイルはカレントディレクトリ下の
node_modulesというディレクトリにインストールされる (存在していなければ作られる) - つまり,
npmを実行したディレクトリごとに「別の環境 (Pythonのvenv相当のもの)」が作られるということになる - インストールされたパッケージ, それが依存しているパッケージは
package.json,package-lock.jsonというファイルに記録される
例
- cowsay は牛がセリフを喋っているアスキー画を生成してくれる他愛のないコマンド
$ npm install cowsay
added 41 packages in 2s
3 packages are looking for funding
run `npm fund` for details
$ ls
node_modules/ package.json package-lock.json
- コマンド (実行可能ファイル) は
node_modules/.bin下にインストールされる
$ ls node_modules/.bin
cowsay@ cowthink@
$ node_modules/.bin/cowsay "I am sleepy"
_____________
< I am sleepy >
-------------
\ ^__^
\ (oo)\_______
(__)\ )\/\
||----w |
|| ||
- ライブラリとして使う場合はホームページを参考にJavaScriptを書き,
nodeコマンドで実行 - 例えば以下のようなファイル
a.jsを,npm install cowsayを実行したディレクトリ (node_modulesがあるディレクトリ) に作り,
var cowsay = require("cowsay");
console.log(cowsay.say({
text : "I'm a moooodule",
e : "oO",
T : "U "
}));
- 以下で実行
$ node a.js
参考: グローバルなインストール (この演習では不要)
node_modulesの下に全てがインストールされるという仕様は, 環境がひとりでに隔離される(開発のためのファイルが普段使う環境を汚さない), 気軽にやり直せる (node_modulesを削除すれば良い) という意味では便利だが, 普段から色々なところで使うソフトを入れる場合には不便でもある-gを使うと, グローバルなディレクトリにインストールされる
$ npm install -g パッケージ名
- 「グローバルなディレクトリ」とは,
nvmを使って導入したnpmの場合, あくまでユーザのホームディレクトリの下 (~/.nvm) の下であって, 全ユーザ共通ということではない
ソースコードのダウンロード
- Python同様, 多くのプロジェクトは github にレポジトリがある
- git clone でソースツリーを入手したら, 通常そのトップディレクトリに
package.jsonがある - ソースツリーに入って
npm install(引数なし)を実行すると, 依存しているパッケージだけ (package.json,package-lock.jsonから読み込まれる) が,node_modulesへインストールされる - 例:
$ git clone git@github.com:piuccio/cowsay.git
$ cd cowsay
$ npm install
npm install パッケージ名としたときと異なり, 本体 (cowsay自身)はインストールされないことに注意
開発用のビルド
- 一般にJavaScript処理系 (
nodeやブラウザ) はソースファイルを直接実行できるため, 特別なビルドのステップは不要な場合が多い - 必要なパッケージもあり, そのような場合は,
npm installのあと,
$ npm run build
を実行する
- これが必要か否かは,
package.jsonの"scripts"という属性の中に,"build"という属性があるかで判断する (npm run buildはpackage.json-> "scripts" -> "build" に書かれているコマンドを実行する) - この状態で, パッケージを使う準備がほぼ整っているが, 上述した動作 (ソースツリー内で
npm installを実行してもそのパッケージ自身はインストールされない) のためやや注意が必要
(a) node_modules/.bin 下にコマンドがインストールされないため, コマンドの動作を追跡したければそのソースファイルを突き止める必要がある
- それは
package.jsonの"bin"という属性を見るとわかる
"bin": {
"cowsay": "./cli.js",
"cowthink": "./cli.js"
},
:の左側がインストールされた後のコマンド名, 右側がそのソースコード(ソースツリーのトップディレクトリからの相対パス)- これを見て, ソースツリー直下の
cli.jsがcowsayコマンドの実体であるとわかる
(b) node_modules/ 下にインストールされないため, ライブラリとして使いながらデバッグする場合も, require(パッケージ名) ではダメで, require(パス名) とする必要がある
- 例えば以下の
require("cowsay")はnode_modules下にcowsayがないため失敗する
var cowsay = require("cowsay");
- 代わりに (
git cloneを実行したフォルダで実行するのであれば)
var cowsay = require("./cowsay");
とする.
- または
cowsayフォルダで実行するのであれば,
var cowsay = require(".");
でもよい
パッケージの検索
npm searchコマンドで検索することもできる
npm search キーワード
- npm ホームページで検索することもできる
練習
- JavaScript のパッケージ
cowsayのgithubレポジトリを見つけ, - 空のディレクトリを作り, コードをダウンロード (git clone),
- 開発用インストール
cowsayディレクトリに入ってnpm install- (任意)
cowsayの親ディレクトリでnpm install ./cowsayを行ってみよ
ソースコードを検索する
- ソースコードを入手して, ビルド, 実行できるようになったらコードを修正する準備が整ったことになる
- どんな修正をするにせよ, 何か目標が決まったら, ソースコード内でそれに関連するコードがどこに書かれているかを突き止めるのが最初の仕事になる
- そのための方法はいくつかある
- 汎用的な文字列検索 (grepなど) ...
- 指定した文字列や正規表現が含まれえいるファイルやその行を教えてくれる
- ディレクトリを指定してその下の全てのファイルを対象にすることもできる
- どんなファイルも検索対象にできる汎用性があるが, ファイルの中身をプログラムとして解釈してくれるわけではないので, 「この変数が定義されている場所」とか「この関数が呼び出されている場所」のような検索はできない
- クロスリファレンスツール (globalなど)
- プログラムの中身をある程度解釈して, 「この変数が定義されている場所」とか「この関数が呼び出されている場所」のような検索ができる
- ただし実際にプログラムをビルドしているわけではないので不正確なことも多い (例えば同じ名前の関数が複数あり, 環境ごとに異なるものが使われるような状況で, 実際には使われていない関数が見つかってしまうなど)
- 汎用的な文字列検索 (grepなど) ...
- どちらも, プログラムのどこが実際に実行されているかなどを教えてくれるものではない
- 実際に何が実行されているかを突き止めるためにはデバッガによる追跡が強力であり, ぜひそれをマスターしてほしい
- しかし静的な検索ツールも手軽さや, 大規模なコードに対しても簡単に適用できるという利点もあり, 両方をうまく使い分けることが必要
- 例え話としては, 静的な検索ツールは「森全体」を把握するツール, デバッガは木を一本ずつ観察するツール
grep : 汎用ファイル・ディレクトリ検索
基本
grep 文字列 ファイル
で, 「ファイル」から「文字列」を検索する
具体的には, ファイル中に「文字列」が含まれる行があればそれらの行を表示する
複数ファイル
grep 文字列 ファイル ファイル ファイル ...
与えられた全てのファイルを検索する
- 例:
$ grep *.c *.h
でカレントディレクトリの全ての .c, .h で終わるファイルを検索する
ディレクトリの再帰的探索
grep -r 文字列 ディレクトリ
「ディレクトリ」下の全てのファイルを検索する
-
ソースコード全体を一網打尽に検索できる強力なコマンドであるが, コンパイルによってできたオブジェクトファイルや実行可能ファイルなども対象にしてしまうので, それを排除するために
-I(バイナリファイルを検索対象から排除するオプション) をつける -
例:
$ grep -rI 文字列 .
はカレントディレクトリ下の, バイナリファイルを除く全てのファイルを検索する
正規表現の検索
- これまでは与えた文字列の完全一致を含む行だけを見つけるものであったが, その代わりに正規表現を用いることも可能 (
-eオプション) - 例えば以下は,
intが行頭に現れる行を, カレントディレクトリ内のバイナリファイルを除く全てのファイルから検索する
$ grep -e '^int' -rI .
クロスリファレンスツール
now writing ...
デバッガで動作を追跡する
前口上
- デバッガは文字通りデバッグをするためのものだが, 実際の機能はプログラムを, その状態 (どの文を実行しているか, 変数の値がどうなっているか) を表示しながら少しずつ実行する, というものである
- したがってバグがない状態でも プログラムの動作・仕組み を知るのに最適なツールである
- 特に, まだ仕組みがわからないのプログラムが, どこでなにをしているか, 自分が実現したいことに関係した機能はどこ (どのファイルのなんという関数) で行われているかを調べることができる
- ある機能がどこで行われているかを探るのに, 勘 (ファイル名や関数名) や静的なソースコード検索 (grepなど) で探すこともひとつの方法ではあるが, 一見関係しそうで実は無関係とか, その環境では全く使われていないコードなどが見つかってしまうこともよくある
- デバッガは 実際に実行されているコード に限定して追跡ができ, 静的な検索よりも確実性高く, 関連するコードを突き止めることができる
- この演習で, デバッガを使ったコードの追跡をぜひ身につけてください
デバッガの機能
基本はプログラムを「少しずつ, 止めながら実行する」
-
そのやり方に何種類かある
-
デバッガやデバッグ環境によって微妙に呼び方が異なる場合があるが, 概念はどれもほとんど同じ
-
nextまたはstep over: 次の行まで実行する. ただしその行に関数呼び出しが含まれていたらそれらすべてを飛ばす. つまり文字通りプログラム上の次の行まで実行する (関数呼び出しの最後の文の場合はその関数の終わりまで) -
stepまたはstep into: 次の行まで実行するが, その行に関数呼び出しが含まれていたらその関数の中に入っていく (呼び出している関数の先頭まで実行する) -
finish,returnまたはstep out: 現在実行中の関数の終了まで実行する -
continue: 明示的に設定した「次に止めるポイント (breakpoint)」まで実行する -
breakpoint: continueを実行したときに止めるポイントを設定する -
どれも目的は同じで, プログラムの実行を前に進める それだけ
-
どこまで進めるかが違うだけ
例え話:
next,step over= 各駅停車 : 各行 (駅) で止まるstep,step into= 途中下車の旅 : 各行 (駅) で止まるのみならず, 各駅で下車して寄り道をする (呼び出している関数の中に入る). やや例えが苦しいが勘弁continue= 急行 : 次の停車駅まで止まらずに通過する
変数や式の値を調べる
-
環境によってやり方は異なり, 明示的にコマンドを打って表示する環境もあれば, 統合環境のように画面を分けて局所変数の値を一覧にしてくれる環境もある
-
print <式>: 明示的にコマンドを打って表示する環境では,printなどのコマンド名で式 (特別な場合として変数) を表示できることが通常 -
統合環境 (VS Codeなど) では画面を分けて局所変数の値を自動的に一覧にしてくれる
- それでも単なる変数値だけでなく, 配列の特定の要素値や, 複雑な計算をした値を表示したい場合もあるので, それを能動的にできるようにすることが重要
-
up,down: 今実行が停止している関数だけでなく, それを呼び出している関数, 更にそれを呼び出している関数, ... に焦点を切り替えて, 変数や式を表示することができるupは焦点を caller (今焦点が当たっている関数を呼び出した関数) 側にひとつずらすdownは焦点を callee (今焦点が当たっている関数から呼び出された関数) 側にひとつずらす- どちらの場合も
up,downをした後はprint <式>を通常通り実行できる (使える変数が違うだけ)
-
VS Codeのような統合環境ではスタックトレース (今実行が停止ている関数, それを呼び出した関数, それを呼び出した関数, ... ) が自動的に表示されておりその中から焦点を当てたい関数を選べば同じ効果が得られる
デバッガを使ったソースコードの追跡・調査 --- 思考過程
- デバッガを使って, ソースコードのどこで自分の興味がある機能が実現されているかを追跡・調査することができる
- 基本は,
- プログラムの先頭 (例: C/C++であれば main関数の先頭, PythonやJavaScriptであればファイルの先頭) で実行を止める
- そこから「少しずつ実行」していき, 関係がありそうなコードにたどり着く
- 問題は,「少しずつ実行」していくといっても文字通り1行ずつ実行して行くと永遠に時間がかかってしまうこと
- そこで以下では興味ある場所を発見するための「作戦」「思考過程」を, もう少しそれらしく言語化する
- 当初は next (step over) を基本にして, 関数呼び出しは飛ばしながら実行する
- つまりC/C++言語であればmain関数中の各行, PythonやJavaScriptであればファイルトップレベルの各行に停車してくイメージ
- 今止まっている行 (次に実行する行) で呼び出されている関数が, 関数名などから明らかに自分の興味に関係しそうであれば step (step into) で関数の中に入っていく
- step (step into) で入ってみて違うなと思ったら finish (step out) で脱出
- 逆に, next (step over) で飛ばしたら実は興味ある関数を飛ばしてしまった (行き過ぎた), ということもある
- そうなったことは典型的には, 外から観測できるプログラムの出力で判断できる. 例えばプログラムが "hello" と出力するのを変更したいのに, "hello" と表示されてしまったら明らかに行き過ぎたことになる
- 行き過ぎが判明したら, 基本はもう一度プログラムの先頭からの実行をやり直す
- 同じところまで next (step over) でたどり着いて今度は step (step into) するのが原始的なやり方
- 慣れてきたらそうする代わりに, 行き過ぎた行に breakpoint を設定してそこまで continue で一気に実行(急行) するのがよい
- これを通して「どの関数の中で興味ある処理が実行されているか」ということに関する知識が, 一段精度高く得られたことになる
- あとは同じことをその関数の中でも繰り返していき, 徐々にその精度を高めていく (大きな配列中から目当ての値を2分探索しているイメージと言えば伝わるか?)
ひとたびどの関数で興味ある処理が行われているかの当たりがついてきたら, その関数の中の処理の流れや, データの流れを見ていく
- 特に, 必要な情報がデータ (変数, 配列, 構造体など) としてどう表現されているかを突き止め, それがどう計算されているかの元をたどっていくことが主な目標になる
- 目当てのデータがどの変数や配列に入っていそうかを調べるには端末のデバッガであれば
printコマンド, VS Codeであればの変数表示のペインやDebug Console で式の値を表示する機能を活用して当たりをつける - 変数に当たりがついたらそれがどこから渡ってきているかを, ソースコードを見ながら考える
- 見ている関数内で計算されている場合もあれば, 引数として渡されている場合もある
- 前者であれば, それを計算するのに使われている別の変数 (「親」の変数とでも言うべきか) が見つかるかもしれない
- 後者であれば, 呼び出している関数 (親関数) の中を観察することが重要になる
- それには, 端末内のデバッガであれば
upコマンド, VS Code であればスタックトレースからcallerを選択することで, 親関数の状態が観測可能になる - 親関数にたどり着いたら同様に, そのデータを親関数自身が計算しているか, さらに親から渡されているかなどを追跡していく
- このようなことを繰り返して, 変更したいデータの発生から完成に至るまでの流れを突き止めていくことができる (ことが多い)
実際は実践あるのみ
- 以上がデバッガを使ってソフトウェアの動作を解明していくときの「思考過程」
- 抽象的で, 読むだけですぐに腑に落ちることは少ないと思うので, 以降のページの実例を見て, 自分でも実践して, 習得してください
C/C++ (gdb) 編
練習
- GNU awk もしくはお手頃な C/C++ で書かれたソフトに変更を施してみよ
- 題材は何でもよい (目標はあくまで開発用インストール手順や, デバッガの使い方の練習)
- 以下の動画では GNU awk を「未定義変数が参照されたらエラー」というお題を例にとって説明しているのでそれをやるのでもよい
Python (pdb) 編
練習
- art もしくはお手頃な Python で書かれたソフトに変更を施してみよ
- 題材は何でもよい (目標はあくまで開発用インストール手順や, デバッガの使い方の練習)
- 以下の動画では art に「等幅表示オプションを追加する」というお題を例にとって説明しているのでそれをやるのでもよい
JavaScript (node.js) 編
練習
-
cowsay もしくはお手頃な JavaScript で書かれたソフトに変更を施してみよ
-
題材は何でもよい (目標はあくまで開発用インストール手順や, デバッガの使い方の練習)
-
以下の動画では cowsay に「尻尾を変更するオプションを追加する」というお題を例にとって説明しているのでそれをやるのでもよい
-
orz : 以下の動画, 何故か途中から絵が止まっています (声だけが先へ進んで画面が動かない)
-
しばらくしたら取り直しますが上記問題設定と動画前半で後は自分でやってみてください