結論

npm error code ENOTEMPTY は、移動しようとした先が空でないディレクトリだったという意味です。npm はこのコードに専用の説明を持っておらず、既定の扱いになるため、画面に出るのは OS が返した1文だけです。

ここで押さえるべき点があります。npm は導入の過程で、置き換える対象を一度別名へ退避します。この移動が ENOTEMPTY で失敗した場合、実装は例外を握りつぶし、退避先を中身ごと削除してから移動をやり直します。つまり、単に古い退避先が残っていただけなら表には出ません。

したがって画面に出た時点で、それは2回目の失敗です。退避先を削除できなかったか、削除した直後に誰かが作り直したかのどちらかに絞れます。前者はファイルシステムの制約、後者は同時に動いている別のプロセスです。

読む場所は pathdest の2行です。path が退避される元、dest が退避先で、後者はドットで始まる名前になります。この名前は元の経路から機械的に決まるため、実行のたびに変わりません。

エラーが発生する処理段階

ENOTEMPTY は取得の段階では出ません。依存の解決も取得も終わり、実際に node_modules を書き換える段階で起きます。

第一段階は差分の計算です。npm は今ある木と目標の木を比べ、変更するものと削除するものを列挙します。

第二段階が退避です。変更または削除の対象になった浅い階層のものを、別名へ移動します。ここが ENOTEMPTY の主な発生場所です。退避しておく理由は、途中で失敗したときに元へ戻せるようにするためです。

第三段階が展開で、新しい内容を書き込みます。第四段階で退避したものを片付けます。

失敗が第二段階で起きると、npm は元へ戻す処理を試みます。このとき戻す方向の移動でも同じコードが出ることがあります。pathdest の関係が逆になっていれば、戻す側で失敗しています。

最初に確認すること

まず、診断用の行を抜き出します。

npm install 2>&1 | grep -E "npm error (code|syscall|path|dest|errno)"

出力はこの形になります。

npm error code ENOTEMPTY
npm error syscall rename
npm error path /app/node_modules/lodash
npm error dest /app/node_modules/.lodash-Ab3dEf9x

dest の名前に注目してください。ドットに続けて元の名前があり、その後ろに8文字の英数字が付きます。実装では、元の経路をもとに固定の手順で短い文字列を作り、.<元の名前>-<その文字列> という名前にします。経路が同じであれば同じ名前になるため、何度実行しても変わりません。

次に、その退避先が実際に残っているかを見ます。

ls -a node_modules | grep "^\."

dest と同じ名前が出てくれば、前回の中断が残っています。出てこないのに失敗する場合は、削除した直後に作り直されています。

同時に動いているものも確認してください。

ps -ef | grep -E "npm|node" | grep -v grep

原因別の確認方法と解決策

原因1:監視ツールや開発サーバーが動いたままになっている

最も見落とされる形です。開発サーバーやファイル監視ツールが node_modules を読み書きし続けていると、npm が退避先を消した直後に作り直されます。

npm は一度目の失敗を自動で処理するため、たまたま残っていただけなら表に出ません。表に出たということは、消した直後に何かが動いています。

確認方法は、止めてから比べることです。

npm install

監視ツールを止めた状態で同じコマンドを実行し、結果が変わるかを見ます。変われば確定です。

対処は、node_modules を触るものをすべて止めてから導入することです。ビルド監視、テストの継続実行、統合開発環境の自動処理などが該当します。導入が終わってから起動し直してください。

原因2:npm が同時に複数走っている

継続的インテグレーションで複数のジョブが同じ作業ディレクトリを共有している場合や、統合開発環境が裏で導入を始めている場合です。

確認方法はプロセスの照会です。

ps -ef | grep npm | grep -v grep

同じ作業ディレクトリを指すものが2つ以上あれば確定です。継続的インテグレーションでは、ログの時刻を突き合わせて、複数のジョブが重なっていないかを確認してください。

対処は直列化と分離です。同じディレクトリへの導入が重ならないようにし、可能であればジョブごとに作業ディレクトリを分けます。

concurrency:
  group: install-${{ github.ref }}
  cancel-in-progress: false

キャッシュの復元と導入が同時に走る構成でも起きます。順序を明示してください。

原因3:前回の中断で退避用のディレクトリが残っている

導入を途中で止めた、容量が足りずに終わった、通信が切れた、といった経緯のあとに起きます。

通常はこの状態でも npm が自動で処理します。それでも失敗する場合は、残っている退避先を消せていません。中身が読み取り専用になっている、所有者が違う、といった事情が重なっています。

確認方法は一覧です。

ls -la node_modules | grep "^d.*\s\."

dest 行と同じ名前が出れば確定です。対処は取り除いてからの再実行になります。

ls -d node_modules/.*-????????

対象を目で確認してから消してください。node_modules の中には .bin.package-lock.json のように、ドットで始まる正規のものもあります。まとめて消さないでください。

rm -rf node_modules
npm install

判断に迷う場合は node_modules ごと作り直すほうが安全です。記録ファイルは残してください。

原因4:割り当てたファイルシステムの制約

コンテナの割り当てや、ネットワーク越しの共有ファイルシステムに node_modules を置いている場合です。ディレクトリの移動が期待どおりに働かず、削除も即座には反映されません。

確認方法は場所を変えて比べることです。割り当ての対象外にある経路へ置いて、同じコマンドを実行します。そこで成功すれば、割り当て側の問題です。

対処は、node_modules を割り当ての対象から外すことです。手元のディレクトリ全体を割り当てている場合、その内側だけを別扱いにできます。

services:
  app:
    volumes:
      - .:/app
      - /app/node_modules

この書き方は、/app/node_modulesコンテナの内部に置き、手元の内容で覆わないようにするものです。導入はコンテナの中で行ってください。

原因5:Windows で他のソフトがファイルを掴んでいる

Windows でのみ起きる場合です。ウイルス対策ソフトが導入直後のファイルを検査していると、その間は移動も削除もできません。編集ソフトが開いている場合も同様です。

npm は移動が権限の問題で失敗したときに、複製へ切り替える経路を持っています。ただし空でないディレクトリの移動については、退避先を消してからやり直す動きになるため、掴まれ続けていると解消しません。

確認方法は、条件を外して比べることです。作業ディレクトリを監視の対象から外し、編集ソフトを閉じてから同じコマンドを実行します。

対処は、作業ディレクトリを常時監視の対象から外すことです。組織の方針で変更できない場合は、導入の間だけ止められるかを管理者に確認してください。監視を無効にしたままにする対処は勧められません。

近いエラーとの境界

EEXIST は、移動先が既に存在する場合です。npm の実装では ENOTEMPTY と同じ分岐で扱われ、退避先を消してからやり直します。表に出た場合の調べ方も同じになります。

EPERM は操作そのものが許されていない場合です。npm はこのコードのときに移動をあきらめ、中身を1つずつ複製する経路へ切り替えます。Windows で使用中のファイルに対して出ることが多く、ENOTEMPTY とは扱いが分かれます。

EACCES権限による拒否です。対象は存在しますが読み書きできません。npm は操作が OS に拒まれたと説明します。

ENOENT は対象が見つからない場合です。npm の実装は、移動の途中でこのコードが出たときには親ディレクトリを作ってからやり直します。

ENOSPC は容量の不足です。容量不足で導入が途中で止まると、次の実行では原因3の形になります。前後関係で入れ替わる点に注意してください。

内部動作または公式仕様

導入の実処理は Arborist が担当します。木を書き換える際、変更または削除の対象になった浅い階層のものを先に退避します。

退避先の名前は固定の手順で決まります。元の経路をもとに短い要約を作り、記号を取り除いて先頭8文字を取り、.<元の名前>-<その8文字> という名前を組み立てます。要約の材料は経路そのものなので、同じ場所に対しては常に同じ名前になります。作業ディレクトリを変えれば名前も変わります。

移動そのものは、まず名前の変更を試み、それが装置をまたぐ場合や権限の問題で失敗した場合にだけ、中身を1つずつ複製する経路へ切り替えます。この切り替えは ENOTEMPTY では起きません。

ENOTEMPTYEEXIST は別に扱われます。実装は移動の失敗を受け取ると、この2つの場合に限り、移動先を中身ごと削除してから移動をやり直します。ここで再度失敗すると、その例外は処理されずに上へ伝わります。画面に出るのはこの2回目の失敗です。

この設計から、表に出た ENOTEMPTY の意味が絞られます。削除が効いていないか、削除と再移動の間に移動先が作り直されているかのどちらかです。単に古い退避先が残っているだけであれば、1回目の処理で解消します。

なお npm 側にこのコードの説明文はありません。実装のコードごとの分岐に該当がなく、既定の扱いになります。表示されるのは OS が返した1文と、診断用の項目だけです。

バージョン差・注意点

出力の接頭辞が変わりました。現在の npm は npm error で始まり、古い版は npm ERR! でした。ENOTEMPTY については専用の分岐が無く既定の扱いのままなので、説明文の違いは接頭辞だけです。

対処として npm cache clean --force を挙げる記事が見られますが、ENOTEMPTY は取得の段階ではなく node_modules の書き換えの段階で起きます。キャッシュを消しても移動先の状態は変わりません。

rm -rf node_modules && npm install は原因3に対しては有効です。ただし原因1と原因2では、作り直している側を止めない限り再発します。まず何が動いているかを確認してください。

退避用のディレクトリを手作業で消す場合は、対象を必ず目で確認してください。node_modules の直下には .bin.package-lock.json のように、ドットで始まる正規のものが存在します。ドットで始まるものを一括で消すと、導入済みの状態が壊れます。

管理者権限を付けて実行する対処も見られますが、ENOTEMPTY の多くは権限の問題ではありません。付けて通った場合、node_modules の所有者が変わり、次回以降も同じ権限が必要になります。

Editor’s Note

npm がこのコードを内部で一度処理している点は、調査の前提を変えます。実装を確認すると、移動の失敗を受け取る箇所で EEXISTENOTEMPTY だけが特別扱いされ、移動先を中身ごと削除してから移動をやり直す作りになっています。

当時の状態としては、この扱いが入る前は、前回の中断で残った退避用のディレクトリがそのまま失敗の原因になっていました。そのため古い解説の多くは「残骸を消せば直る」という対処で完結しています。実際、その時期にはそれで解決していました。

現在も適用できるかという点では、対処としては有効ですが、説明としては不足しています。残骸が残っているだけの状態は、現在の npm が自分で解消します。それでも表に出たということは、削除が効いていないか、削除の直後に作り直されているかのどちらかです。残骸を消すだけでは、次の実行でも同じ場所で止まります。

読み替えの規則は次のとおりです。古い記事の対処である rm -rf は今も試す価値がありますが、それで解消しない場合を「よくある例外」ではなく「本来の姿」として扱ってください。node_modules を触っているものが他にあるか、という問いが本筋になります。

参考資料


免責事項:本記事の内容は、執筆時点の公開情報をもとに作成したものです。ソフトウェアの仕様は予告なく変更されることがあります。最新の情報は各ツールの公式サポートページをご確認ください。本記事の情報を利用した結果生じたいかなる損害についても、著者および運営者は責任を負いかねます。