Repository navigation
Description
Coding agents increasingly work on a Phoenix app in parallel, each in its own git worktree (Claude Code --worktree and subagents with worktree isolation, Conductor, worktrunk, git-cow). On copy-on-write filesystems (APFS clonefile, btrfs/XFS reflinks) a new worktree can get _build and deps from the main checkout for free, in milliseconds. Since Elixir v1.15 Mix then recompiles the whole project anyway, because the manifest's cwd is part of the compiler cache key (elixir-lang/elixir#12462): every .beam names its source by absolute path, and __DIR__ is absolute too. elixir-lang/elixir#13241 discussed this for CI caches; --no-check-cwd (v1.19.2) skips the check but leaves the stale paths in place (stack traces, ex_doc links, __DIR__).
I measured what it takes to move a build correctly instead, on a Phoenix 1.8 app (67 modules plus a path dependency, Elixir 1.20.4, OTP 28):
-
Full recompile after the move: 3.8 s, the dominant cost of creating a worktree.
-
Rewriting the old root to the new one instead: 0.14 s, after which Mix compiles nothing except the
embed_templatesmodules (below). The paths live in a small, known set of places:- the compile manifest:
cwdand the local deps config - .beam chunks:
CInf(:source),Dbgi(:file),Attr(e.g.@external_resource),LitT(compile-time literals such as__DIR__) - the
.appfile (compile-time config computed from__DIR__)
After that,
module_info(:compile)[:source], stack traces, Tidewave and LiveDebugger all point into the new checkout. It is ~100 lines of:beam_lib. - the compile manifest:
-
The remaining recompiles come from
embed_templates:__mix_recompile__?/0compares an md5 of the templates' absolute paths (__mix_recompile__?/0hashes absolute template paths, so a moved or cloned project recompiles everyembed_templatesmodule phoenix_template#15, with a two-line fix).
End to end, a new worktree with its own database (PostgreSQL 18 CREATE DATABASE ... STRATEGY FILE_COPY with file_copy_method = clone) and its own mix phx.server is serving in ~2.8 s instead of ~35 s.
Ideas, in order of ambition (most of it lives in Mix, but Phoenix apps are where this hurts most):
mix compile --relocate-from OLD_ROOT, or relocation done automatically when only the cwd changed: Mix rewrites the paths above instead of recompiling. Everything it needs is already in the manifest.- Exact relocation: the compiler knows when it expands
__DIR__/__ENV__.file; recording that per module would let Mix recompile only the modules that captured the old root in a way it cannot rewrite. - Longer term: store source paths relative to the project root in .beam files (as
:relative_pathsalready does for quoted nodes and diagnostics) and resolve them at runtime. - Phoenix side: path-independent recompile checks (phoenix_template#15), and perhaps generator defaults that avoid baking
__DIR__-derived absolute paths into compile-time config.
This would make worktree-per-agent setups start in a couple of seconds instead of paying a full compile per task, and would also help CI caches and container builds that move _build between paths. Happy to work on PRs for whichever of these you'd accept.
Activity
SteffenDE commentedon Oct 1, 2026
I think there's not much to do on the Phoenix side after phoenixframework/phoenix_template#16?
rhcarvalho commentedon Oct 14, 2026
I think there are only two things left,
:esbuildand:tailwindconfigs inconfig/config.exs.They depend on
__DIR__for:cdand:env/NODE_PATH.Details
Both
:cdand:envare passed toSystem.cmd->Port.opencd-> C'schdir: either absolute or relative path would work. Need to check the cwd guarantees when theSystem.cmdcalls are done.Details
env->NODE_PATHis supposed to contain absolute paths. But we may be able to move away from it, e.g. using symlinks innode_modules-- need to check the Windows story.Details
The paths we add to NODE_PATH:
deps: this is to makephoenix,phoenix_live_view,phoenix_html, anddaisyuiavailable to JS/CSS assets without requiringnpmandnode_modules. They get there viamix deps.get.Mix.Project.build_path(): for colocated assets generated into_build/<env>/phoenix-colocated/<app_name>.@onnimonni would you like to investigate this further?
onnimonni commentedon Oct 31, 2026
Follow-up after looking into the remaining Phoenix-side pieces. I tested a relocated copy (APFS clone of the project including
_build, compiled withcheck_cwd: false).Still broken after phoenix_template#16, now with PRs:
__mix_recompile__?still globs the absolute root captured at compile time. A template added in the copy was silently never compiled (function_exported?(MyAppWeb.Layouts, :extra, 1) == false) until something else touched the module. The fix stores the root relative to cwd. The new test fails on main.@after_verify) printed the original checkout's absolute paths. Example:└─ /original/checkout/lib/my_app_web/live/foo_live/form.ex:24. An editor or agent working in the worktree then edits the wrong copy.~proute warnings (warn_location).@rhcarvalho, on
:esbuild/:tailwind__DIR__configs: I don't think they need changes for this use case.config/config.exsis evaluated on every Mix invocation, so in the relocated copyApplication.get_env(:tailwind, :my_app)[:cd]andNODE_PATHalready point at the new directory. Nothing from them ends up in the.appfile or in compiled modules. They would only matter if the evaluated config were persisted, e.g. in a release built in one place and run from another.Remaining absolute paths in a relocated build:
CInfsource in every module: IExopen, ex_doc.debug_heex_annotationscaller data ({module, fun, file, line}literals in every LiveView, used for editor jump links).__DIR__).I'm prototyping idea 1/2 in Mix:
CInf/Dbgi/Docschunks.I'll post numbers when it works.
onnimonni commentedon Nov 1, 2026
Follow-up with numbers for ideas 1/2: relocation in Mix, plus making every code-level path relative.
Libraries: no compiled code captures the project root anymore. Besides the three PRs above, these PRs remove the remaining absolute paths from compiled code:
@external_resource.@external_resourcerelative.Ecto.QueryError, at 28 places in the query builders.With all of them, a scan of the
.beamchunks of a generated app (phx.new,phx.gen.auth, 40phx.gen.liveresources, 195 modules) finds the absolute root only in compile metadata:CInf:sourceandDbgi.Mix relocation prototype (onnimonni/elixir@relocate-build, one commit on Elixir main, ~135 lines in
Mix.Compilers.Elixirplus 2 tests):CInf,Dbgi,Docs).__DIR__, and only those are recompiled.--no-relocateopts out.module_info(:compile)[:source]points into the new checkout.New worktree (APFS clone of the project including
_build), 5 interleaved rounds:--no-check-cwdWithout the library PRs, 131/195 modules capture the root (LiveView component calls), and relocation only saves ~5%.
I didn't try making
:source/debug info relative (idea 3). It runs into what was discussed in elixir-lang/elixir#14744: dependencies compile with their own cwd, escripts/archives need absolute paths, and cover/IExopen/ex_doc read:source. Relocation fixes the reason for the cwd check (elixir-lang/elixir#12462, stale:source) for ~80ms. If that direction sounds acceptable, I'll open an Elixir issue with the patch.rhcarvalho commentedon Nov 1, 2026
Status:
:esbuildand:tailwindconfigs in themix phx.newinstaller templatesIf you think getting input from other folks in the community would be beneficial to set direction, I'd suggest following https://github.com/elixir-lang/elixir#proposing-new-features and starting a discussion in the mailing list. For small and obvious wins, I think focused PRs are likely to be welcomed by the Elixir team.
I'll close this as the part related to Phoenix seems to be complete. Thanks @onnimonni!