상황
변경마다 깨끗한 작업 트리를 새로 만들어 빌드하는 습관은 좋다. 작업 중인 체크아웃의 잔여물이 결과에 섞이지 않기 때문이다. 문제는 의존성이다. 수백 메가바이트짜리 의존성 폴더를 트리마다 다시 설치하는 건 느리고, 설치 도구 자체가 호스트 런타임 버전과 맞지 않아 실패할 때도 있다. 그래서 가장 먼저 떠오르는 지름길이 원본 체크아웃의 폴더를 심볼릭 링크로 걸어 두는 것이다.
무엇이 깨지는가
링크를 건 트리에서 빌드를 돌리면 코드와 무관한 오류가 난다. 요지는 "이 모듈의 경로가 파일 시스템 루트 밖을 가리킨다"는 것이다. 요즘 번들러는 보안과 캐시 일관성을 위해 프로젝트 루트를 경계로 삼고, 모듈을 불러올 때 심볼릭 링크를 끝까지 풀어 실제 경로를 본다. 그 실제 경로가 다른 체크아웃 안에 있으니, 번들러 입장에서 그 파일들은 프로젝트 밖의 낯선 파일이다. 설치는 성공했고 링크도 정상인데 빌드만 실패한다.
왜 헷갈리는가
이 오류는 방금 한 변경을 의심하게 만든다. 새로 만든 트리에서 처음 돌린 빌드가 실패했으니 당연하다. 하지만 같은 커밋을 원본 체크아웃에서 빌드하면 멀쩡히 통과한다. 차이는 코드가 아니라 트리를 준비한 방식에 있다. 두 결과를 나란히 놓고 보기 전까지는 원인을 엉뚱한 곳에서 찾게 된다.
해결
작업 트리에 진짜 디렉터리를 준다. 같은 파일 시스템이라면 하드링크 복사가 가장 싸다. 파일 내용은 공유하면서 디렉터리 항목은 트리 안에 실제로 존재하므로, 번들러가 경로를 풀어도 루트 안에 머문다. 파일 시스템이 다르면 일반 복사를 한다. 어느 쪽이든 설치 도구를 다시 돌리지 않아도 되고, 호스트 런타임과 설치 도구의 버전 충돌도 피해 간다.
주의할 점
하드링크는 내용을 공유하므로 트리 안에서 의존성 파일을 직접 고치면 원본도 같이 바뀐다. 의존성을 패치해야 하는 작업이라면 그 트리만큼은 일반 복사를 쓴다. 그리고 작업이 끝나면 트리를 정리한다. 하드링크 트리는 디스크를 거의 차지하지 않는 것처럼 보여서 쌓여 가기 쉽다.
확인 방법
트리를 준비한 직후 의존성 폴더가 링크가 아니라 디렉터리인지 한 번 확인하고, 빌드가 원본 체크아웃과 같은 결과를 내는지 본다. 한 번 이 방식으로 준비 스크립트를 고정해 두면, 다음 사람은 같은 오류를 보고 자기 변경을 의심하느라 시간을 쓰지 않는다.
The setup
Building every change in a fresh, disposable worktree is a good habit: nothing left over in your working checkout leaks into the result. Dependencies are the friction. Reinstalling a dependency folder of several hundred megabytes per worktree is slow, and sometimes the install tool itself breaks against the host's runtime version. So the first shortcut that comes to mind is to symlink the main checkout's folder into the new tree.
What breaks
Run the build in the linked tree and you get an error that has nothing to do with the code. Paraphrased, it says a module path points outside the filesystem root. Modern bundlers treat the project root as a boundary for security and cache consistency, and when they load a module they resolve symlinks all the way to the real path. That real path lives inside a different checkout, so as far as the bundler is concerned those files are strangers from outside the project. The install succeeded, the link is fine, and the build fails anyway.
Why it misleads
The error makes you suspect the change you just made — the first build in a brand-new tree failed, after all. But build the same commit in the original checkout and it passes cleanly. The difference is not in the code; it is in how the tree was prepared. Until you put those two results side by side, you will be looking for the cause in the wrong place.
The fix
Give the worktree a real directory. On the same filesystem, a hardlink copy is the cheapest option: file contents are shared, but the directory entries genuinely exist inside the tree, so when the bundler resolves paths it stays inside the root. Across filesystems, use a plain copy. Either way you skip the install tool entirely and sidestep any version conflict between it and the host runtime.
The caveat
Hardlinks share content, so editing a dependency file in place inside the tree also edits the original. If the task involves patching a dependency, use a plain copy for that tree. And remove the tree when you are done — hardlinked trees look nearly free on disk, which is exactly why they pile up.
How to check
Right after preparing the tree, confirm once that the dependency folder is a directory and not a link, then confirm the build produces the same result as the original checkout. Pin that into the preparation script once, and the next person who sees this error will not burn an hour doubting their own change.
场景
每次改动都在一个全新的一次性工作树里构建,是个好习惯:工作检出里的残留不会混进结果。麻烦在依赖。每个工作树都重装几百兆的依赖目录太慢,而且安装工具本身有时会和主机的运行时版本不兼容而直接失败。于是最先想到的捷径,就是把主检出的依赖目录用符号链接挂到新树里。
哪里会坏
在挂了链接的树里构建,会得到一个与代码无关的错误,大意是“某个模块路径指向了文件系统根之外”。现代打包器出于安全和缓存一致性,会把项目根目录当作边界,并在加载模块时把符号链接一路解析到真实路径。真实路径位于另一个检出里,所以在打包器看来,这些文件是来自项目外部的陌生文件。安装成功了,链接也没问题,构建却失败了。
为什么容易误判
这个错误会让你怀疑刚做的改动——毕竟新树里的第一次构建就失败了。可是在原检出里构建同一个提交,却能顺利通过。差别不在代码,而在树的准备方式。在把两个结果并排比较之前,你会一直在错误的地方找原因。
解决办法
给工作树一个真实的目录。同一文件系统上,硬链接复制最便宜:文件内容共享,但目录项真实存在于树内,打包器解析路径时仍然停留在根目录之内。跨文件系统就用普通复制。两种方式都不需要再运行安装工具,也顺带避开了安装工具与主机运行时之间的版本冲突。
注意事项
硬链接共享内容,所以在树里直接修改依赖文件,原检出里的文件也会一起变。如果任务需要给依赖打补丁,那棵树就用普通复制。用完之后记得删掉工作树——硬链接树看起来几乎不占磁盘,正因如此才容易越积越多。
如何确认
准备好工作树后,先确认一次依赖目录是真实目录而不是链接,再确认构建结果与原检出一致。把这一步固定进准备脚本,下一个遇到同样错误的人就不必花一小时去怀疑自己的改动。
状況
変更ごとにまっさらな使い捨てワークツリーでビルドするのは良い習慣だ。作業中のチェックアウトの残りかすが結果に混ざらない。厄介なのは依存である。数百メガバイトの依存フォルダをツリーごとに入れ直すのは遅く、インストールツール自体がホストのランタイムのバージョンと合わずに失敗することもある。そこで真っ先に思いつく近道が、本体チェックアウトのフォルダをシンボリックリンクで張ることだ。
何が壊れるか
リンクを張ったツリーでビルドすると、コードと無関係なエラーが出る。要旨は「モジュールのパスがファイルシステムのルートの外を指している」である。最近のバンドラは安全性とキャッシュの一貫性のためにプロジェクトルートを境界とし、モジュールを読むときにシンボリックリンクを最後まで解決して実体のパスを見る。その実体は別のチェックアウトの中にあるので、バンドラから見ればプロジェクト外の見知らぬファイルだ。インストールは成功し、リンクも正常なのに、ビルドだけが落ちる。
なぜ迷うのか
このエラーは直前の変更を疑わせる。新しいツリーで最初に回したビルドが落ちたのだから当然だ。しかし同じコミットを本体のチェックアウトでビルドすると、問題なく通る。違いはコードではなく、ツリーの準備の仕方にある。二つの結果を並べて見るまで、原因を見当違いの場所で探し続けることになる。
対処
ワークツリーには本物のディレクトリを渡す。同じファイルシステムならハードリンクでのコピーが最も安い。ファイルの中身は共有しつつ、ディレクトリエントリはツリーの中に実在するので、バンドラがパスを解決してもルートの内側に留まる。ファイルシステムが違うなら普通にコピーする。どちらでもインストールツールを再実行せずに済み、ホストのランタイムとのバージョン衝突も避けられる。
注意点
ハードリンクは中身を共有するので、ツリーの中で依存ファイルを直接書き換えると本体側も変わる。依存にパッチを当てる作業なら、そのツリーだけは普通のコピーを使う。そして作業が終わったらツリーを片付ける。ハードリンクのツリーはディスクをほとんど食わないように見えるからこそ、溜まりやすい。
確かめ方
ツリーを用意した直後に、依存フォルダがリンクではなくディレクトリであることを一度確かめ、ビルドが本体チェックアウトと同じ結果になることを見る。準備スクリプトにこれを一度固定しておけば、次に同じエラーを見た人が自分の変更を疑って一時間を失うことはない。