🦁

nodejsのソースコードのビルド

2023/08/01に公開

今現在(2023-08-01)raspbian はbullseyeベースなのでaptでnodejsをインストールするとnodejs12が入ります。
ただ、zenn-cliがnodejs12以下はサポート切ってたりで、bullseyeのnpmからzenn-cliをインストールしようとすると怒られます。一応zenn-cliは入りますが、警告だが、非推奨だかのメッセージが出ます。

よってraspberry piからapt->npm->zenn-cliという感じに怒られずにええ感じにインストールできない。
単純にnodejs関連のパッケージだけstable bookwormのレポジトリを向くようにしてもいいですが、それでも最新のLTSでないし、どうせ最新のLTS使わないならまあせっかく?なんでビルドしましょうと思ってやってみました。

前提条件

github cliインストール済み
tmuxインストール済み。
sysstatインストール済み(統計取るのに必須。特に取るつもりがないなら不要)。

ソースコードの準備

# gh repo clone用のディレクトリ作成
mkdir -p ~/src/nodejs
cd src/nodejs

githubからーソースコードをダウンロードする

gh repo clone nodejs/node

cd node
# タグ一覧表示 
git tag

# 使いたいバージョンのタグにcheckout
git checkout v18.17.1

以下はプロジェクト直下にいることを前提に説明していきます。

ビルド方法

ビルド方法はnodejsの場合はBUILDING.mdに書いてあります。
大抵のプロジェクトにはローカルのビルド方法が書いてあるので、その通りにやってください。
無い場合はソースコードを読んだり、実際ビルドしたときにエラーが出るlibファイルを見たり、Makefile読んだりで自分で調べましょう。
古いプロジェクトでない限り、どこかにやり方が書かれていて、How to的な環境は整っていると思います。

nodejsの説明はかなり丁寧なので、わかりやすいです。Readmeや環境構築の説明の書き方の例として参考にしたらいいかも。

依存関係

BUILDING.mdに書いてある通り、Debianは下のようなパッケージが必要です。

sudo apt install python3 g++ make python3-pip

このpythonですが、nodejs v18まで(v19もかも未検証)はpython3.9~3.6がないとビルドできないので注意。最新のnodejs v20ならpython3.11でもビルドできます。

configure

ビルドするときのconfigです。
特に気にしなくていいです。

./configure

rootのときに/usr/local/binと/usr/binが干渉しあってしまうのが嫌なら、下のようにインストール先を変えます

# こうすると/usr/local/nodeのディレクトリを使ってその下にlibやbinを作ってくれる。
./configure --prefix=/usr/local/node

使うときには
PATHに/usr/local/node/binへのパスを通すか、
/usr/local/node/bin/nodeのように直接実行します。

build

tmuxで起動
これはssh経由でビルドしていたりして、sshのセッションが中断されることにより、
処理が中断されないように必要です。
tmuxである必要はありませんが、一番考えなくてもいいのがtmuxですし、愛好家は多いのでこれを使うのが良いかと。
tmuxで適当なセッション作っておきましょう。

tmux

ログ置き場

とりあえずビルドして使うだけでも結構時間がかかるので、ログだけは最低でもとっておいた方が
良いと思います。流れてなくなってしまうので。

ログの場所はどこでもいいですが、自分は特に指定がないなら、一般ユーザーでビルドするときは
~/var/log/プロジェクト名/make
みたいなディレクトリを作ってそこに書き込んでいます。
ここではとりあえず、この場所をログ保管場所としておきましょう。

mkdir -p ~/var/log/nodejs/node/make

help

Makefileにはhelpが書かれているので、一度見てみましょう。

# make --helpでなくて、make helpであることに注意。
# make --helpだとmakeコマンドのhelpがでる。
make help

make installでdefaultのインストール先が/usr/localであることや、
make cleanでキャッシュの削除ができること、
make uninstallがあることを確認します(make uninstallできないプロジェクトもある)。

ビルドしてみる

Makefileをビルドしてみます。
何も引数がない場合はmake allとなってすべてビルドするみたいですね。

make -jはビルドするときのjob数の指定です。コア数に応じたjob数を選んでください。

ここではBUILDING.mdにはmake -j4としてビルドせよと書かれていますが、
raspberry piみたいに貧弱なpcなら-j2, -j3の方がいいかも。
これは貧弱なpcだとバックグラウンドの処理が相対的に重くなっているので、ビルド中にデスクトップが止まったりクラッシュする可能性があるためです。

ここら辺は実際にその環境で実行しないとわからないと思います。

コア数が気になるならlscpuや/proc/cpuinfoを見てコア数を調査しておきましょう。

ビルドのログは流れてしまうので下のようにログを取るのがいいかと。
こうしないと失敗時の切り戻しが面倒です。

# こうすると時間経過もログに残せる。
make -j4 2>&1 | awk '{print strftime("%Y-%m-%dT%H:%M:%S"), $0}' | tee ~/var/log/nodejs/node/make/"$(date +'%Y%m%dT%H%M%SZ')".log

pcのスペック的にしんどいかも知りたい人はこの時にtmuxで別セッションを開いておき、
負荷を調べておきましょう。何度もビルドする可能性があるなら測った方がいいです。

mkdir -p ~/var/log/nodejs/node/iostat/
# これをやると1秒間に一回、cpuやディスクioの統計をとってくれる。
iostat -x 1 | awk '{print strftime("%Y-%m-%dT%H:%M:%S"), $0}' | tee ~/var/log/nodejs/node/iostat/"$(date +'%Y%m%dT%H%M%SZ')".log

ビルドが始まったら、tmuxをデタッチ、sshを終了して、ビルドが終わるまでほっときましょう。

ビルド時間中に豆知識

cppをやったことない人はびっくりすると思いますが、warning出まくります。
また、膨大な量のログが出るのでびっくりすると思いますが、よくあることです。
正常なので、とくにエラーで停止しない限りほっときましょう。

自分がraspberry pi4Bでビルドしたところ、3時間から4時間ほどで
iostatで調べるとユーザーレベルでcpuが常に90%代でした。
rasperry pi はmicrosdカードにデータがあるので、ディスクioもある程度ネックになるかと思いましたが、iowaitがほとんど無かったので、統計的にはcpuの方が足引っ張ってますね。

負荷を少しでも下げたい場合、特にraspberrypi でビルドしている人はGUIを切ってやった方がいいかも。
私はraspberry pi4Bで最初にビルドした場合はクラッシュしたのでsystemctl set-default multi-user.targetとしてcuiにしてビルドしなおしました。

これらの数値はあくまで私の環境での話であり、高温、低温下や、microsdカードの空き容量や種類によるストレージの速度、電源が足りているかなどでも速度は変わると思います。
参考までに。

ただ、最新のPCなら30分から1時間で終わりそうなので、新しいPC使っている人はあまり気にしなくていい気もします。

出来立てほやほやを使ってみる

ビルドできたなら使ってみます。
下記のコマンドを実行すると対話形式で立ち上がると思います。

./out/Realease/node

リリースバージョンかつbookwormでも採用されたバージョンなので大丈夫だと思いますが、make installする前に軽く動作確認しときましょう。

install

インストール場所にインストールします。
実行すると依存関係を考えて、binや、includeなどを適切な場所に入ります。
これは2~5分程度で終わります。

aptやdnfなどパッケージ管理ツールでなくMakefileでビルドしたなど、自分で勝手に入れたものはlinuxでは/usr/local/配下に入れるのが普通です。
先ほどの説明通り、下のコマンドを実行すると/usr/local/に各ファイルが入ります。

sudo make install

ビルドした複数のバージョンを分けて使いたい

previewやらなんやら、後自分でビルドしたやつとかを使い分けたい場合は擬似的に仮装環境や
なんとかenvみたいなものを実装する必要があります。

とりあえずビルド時にnode用に深いルートができるように、調節します。

versionにビルドバージョンを書く
ただ、update時や削除時にprefixが無いと正しい位置のnodejsとnpmのライブラリを削除しないので注意。

# 17.2-previewとか最新のmainならunstableとか適当に名前をつける。
version=18.17.1
./configure --prefix=/usr/local/node/version/${version}

あとはsudo make install時に勝手にインストールしてくれる

sudo make install

こうすると/usr/local/node/binにnodeとnpmのシンボリックリンクを置いておくといい感じに
やってくれます。

sudo ln -s /usr/local/node/version/${version}/bin/node  /usr/local/node/bin/node
sudo ln -s /usr/local/node/version/${version}/bin/npm   /usr/local/node/bin/npm
sudo ln -s /usr/local/node/version/${version}/bin/npx   /usr/local/node/bin/npx

なぜ/usr/local/binに実行ファイルを置かないかというと、rootのパスはデフォルトで/usr/binより/usr/local/binのほうが優先なので、/usr/local/binと/usr/binに同じファイルを置くとシステムに影響を与えてしまうからです。

/usr/local/node/binを使いたいときにPathに追加しましょう。

ただ複数のバージョンを使い分ける場合はnodejsとnpmが常に使いたい
バージョンのシンボリックリンクを指しているか注意しましょう。
面倒になるなら下のようななんとかenvを自作してスクリプトとして持っておきます。
ここでは仮にnodelocalsという名前にします。

/usr/local/bin/nodelocals
#!/usr/bin/env bash
# 
# nodejs /usr/local/node/version/\$version version management

#######################################
# nodejs /usr/local/node/version/\$version version management
# Globals:
#   None
# Arguments:
#   All.
# Outputs:
#   format 
# Returns:
#   0 if command success, non-zero on error.
# Example:
#   nodelocals list
#   # out put installed version from /usr/local/node/version/\$version.
#   nodelocals set \$version
#   # set node, npm, npx to /usr/local/node/bin
#######################################
function nodelocals() {

  local i
  local new_array=( $@ )
  local version
  # nodeがない場合は空文字
  local selected_version=$(readlink /usr/local/node/bin/node | awk  -F/ '{print $6}')
  for ((i=0;i<$#;i++)); do
    # if find list flags from args, show installed nodejs destination /usr/local/node/bin/node.
    if [ "${new_array[$i]}" = "list" ]; then
      # 引数がlist以外にあるとおかしい。
      if [ -n "$2" ]; then
        echo 'list is no argument.' >&2
        return 1
      fi
      # echo 使っているものに*がつくようにする。
      local selected
      for version in $(ls -1 /usr/local/node/version); do
	      selected=" "
        if [ "${version}" = "${selected_version}" ]; then
          selected='*'
        fi
        # アスタリスクをそのままecho に通すと展開されるので""をつける。
        echo "$selected" $version
      done
      return 0
    fi
    # localが選ばれたら、
    if [ "${new_array[$i]}" = "set" ]; then
      if [ ${UID} -ne "0" ] && [ ${EUID} -ne "0" ]; then
        echo 'this function use superuser only' >&2
        return 1
      fi
      # version取得
      shift
      version=$1
      local check_version
      local exist=1
      # versionの存在確認
      for check_version in $(ls -1 /usr/local/node/version); do
        if [ "${version}" = "${check_version}" ]; then
          exist=0
        fi
      done
      if [ "$exist" = "1" ]; then
        echo 'select installed version!' >&2
        return 1
      fi
      # symlinkで紐付け
      ln -snf /usr/local/node/version/${version}/bin/node  /usr/local/node/bin/node
      ln -snf /usr/local/node/version/${version}/bin/npm   /usr/local/node/bin/npm
      ln -snf /usr/local/node/version/${version}/bin/npx   /usr/local/node/bin/npx
      return 0
    fi
  done
}

function usage() {
    cat 1>&2 <<EOF
nodelocals
nodejs /usr/local/node/version/\${version} allignment version management tools

USAGE:
    nodelocals [FLAGS] [OPTIONS]

FLAGS:
    list                    Show /usr/local/node/version/\$version installed nodejs version engines.
    set                     Set node, npm, npx to /usr/local/node/bin
    -h, --help              Prints help information

OPTIONS:
    --debug                 Set bash debug Option
EOF
}

function main {

  local i
  local new_array=( $@ )
  for ((i=0;i<$#;i++)); do
    if [ "${new_array[$i]}" = "--help" ] || [ "${new_array[$i]}" = "-h" ]; then
      usage
      return
    fi
    # if find --debug flag from args, start debug mode.
    if [ "${new_array[$i]}" = "--debug" ]; then
      set -x
      trap "
        set +x
        trap - RETURN
      " RETURN
      unset new_array[$i]
    fi
  done
  # reindex assign.
  new_array=${new_array[@]}

  nodelocals $new_array
}

main $@

これに実行権限を与えておきます。

sudo chmod 755 /usr/local/bin/nodelocals

各環境へのbinへのシンボリックリンクのディレクトリを作って置きます。

sudo mkdir -p  /usr/local/node/bin

使い方

# /usr/local/nodeにインストールしたnodejs一覧表示
nodelocals list

# /usr/local/nodeに存在する指定したバージョンのnode, npm, npxを/usr/local/binにセットする。
sudo nodelocals set 18.9.1

これにインストール処理やら、なんやら追加されたらなんとかenvです。
実装じたいは割と簡単だと思います。

ただ、あんまやりすぎるとnodeenvやnvm,nに対する車輪の再発明になるので程々にね。
論点は全部自分で管理したいかどうかかな?
/usr/localみたいなシステムに複数バージョン置いときたいとか。

/usr/local/にインストールされたものを使ってみる

/usr/local/binにパスが通っていたら、ちゃんと実行できるはずです。

# 存在確認
command -v npm
command -v node

node -v
## 対話形式で実行してみる。
node

## zenn-cliをインストールしてみる。
sudo npm install -g zenn-cli

上に書かれている方法でインストール先を変更した場合はちゃんとnodeバージョンのversionごとにnpmのインストール先が変わっているはずです。
確認してみましょう

# bin
ls -1 /usr/local/node/version/${version}/bin
# lib
ls -1 /usr/local/node/version/${version}/lib

uninstall

Makefileによってはアンインストール方法が無いものもあります。
その場合は手動で消すことになりますが、nodejsはuninstallができるように書かれているので、
Makefileの場所で以下のようにすればアンインストールされます。

sudo make uninstall

上の方法でバージョンごとにインストール先を変えた場合は/usr/local/node/${version}/にそのnodejsに依存しているライブラリやバイナリが保存されています。なので、バージョンの環境ごとrm -rfで削除しても良いです。

# nodejs18.17.1の環境を削除する時
version=18.17.1
rm -rf /usr/local/node/version/${version}

先ほど書いたnodelocalsに uninstallみたいなの入れてもいいですが、自分で書く管理コストとか、利便性考えたらあんまないかも。他の人が使うなら必要かも。

updateしたい

アンインストールしてから、クリーン、gitで適切なバージョン、
configure, make,make installとします。

cleanしてから、gitで適切なバージョンにチェックアウトして
make installで上書きという形だとシステムに前のバージョンの依存関係のライブラリが残ります。
気をつけましょう。

## 残っているプロジェクトのMakefileを使ってアンインストール
sudo make uninstall

# キャッシュクリーン
make clean

# 使いたいバージョンにチェックアウト
git checkout v{適切なバージョン}

./configure

make 

sudo make install

まとめ

手順に沿ってやれば出来るので、
特に難しい点は無いと思います。

なんとなく気がついたかと思いますが、Makefileでversionを分けずにsystemにinstallするならプロジェクトを持っておかないと、
アンインストールとupdate時に困ります。プロジェクトは持っておきましょう。

上の手順で実行したら、ビルド時にどの程度負荷がかかっていたのかわかるので、
「俺はnodejsに貢献したい!」とか、「JS王に俺はなる!」みたいな人は負荷が多過ぎたら、
時間がかかり過ぎてやってられないと思います。
おとなしくnodejs自体の開発環境用のPC買いましょう。

Discussion