Skip to content

add odin run -cached flag - #7148

Open
i-api wants to merge 1 commit into
odin-lang:masterfrom
i-api:master
Open

add odin run -cached flag#7148
i-api wants to merge 1 commit into
odin-lang:masterfrom
i-api:master

Conversation

@i-api

@i-api i-api commented Jul 28, 2026

Copy link
Copy Markdown

so we have the option to reuse cached binaries if it doesn't require a rebuild. makes runs after the first much less painful now

@gingerBill lmk how you feel about this, cool / not cool?

@antisaling

Copy link
Copy Markdown
Contributor

Even if we want something like this I think you completely overlooked that we still need checking mtime for statically-linked foreign libs.

@i-api

i-api commented Aug 1, 2026

Copy link
Copy Markdown
Author

you are right

@i-api
i-api force-pushed the master branch 2 times, most recently from 8cb7adb to f537dc1 Compare August 1, 2026 18:10
@i-api

i-api commented Aug 1, 2026

Copy link
Copy Markdown
Author

pushed my static libs fix

@i-api
i-api force-pushed the master branch 7 times, most recently from c0d043e to 94e8c3e Compare August 5, 2026 03:11
@i-api

i-api commented Aug 6, 2026

Copy link
Copy Markdown
Author

@gingerBill @Kelimion please review, thanks

@i-api
i-api force-pushed the master branch 3 times, most recently from d85e187 to 0a3a668 Compare August 10, 2026 04:04
@i-api
i-api force-pushed the master branch 3 times, most recently from 6134980 to 346df61 Compare August 21, 2026 06:28
Reuse a previously built executable when nothing that affects it has changed.

  odin build [-cached] <dir | file.odin>
  odin run   [-cached] <dir | file.odin>
  odin test  [-cached] <dir | file.odin>

The caching existed as -internal-cached; this exposes it, and makes an entry
trustworthy enough to be worth exposing. A hit places the entry at the output
path, so -cached changes nothing about where a build lands or what it produces.

An entry is reused only when all of the following are unchanged:

  * every source file the build reads, including #load'ed files and statically
    linked foreign libraries (.a, .lib, .o, .obj and assembly). Dynamic
    libraries and `system:` imports are skipped, as neither is baked in.
  * the build flags. The cache directory is keyed on source paths alone and the
    entry name carries only the target, so -o:none and -o:speed resolve to the
    same entry; unchecked, the second build is handed the first one's executable.
  * the compiler itself.
  * the environment variables that can reach the output: ODIN_ROOT,
    ODIN_CLANG_PATH, ODIN_ANDROID_SDK, ODIN_ANDROID_NDK,
    ODIN_ANDROID_NDK_TOOLCHAIN and PATH.

The compiler is an input because a new one can turn identical sources into a
different executable while nothing in the source tree reflects it: a `make` that
touches only src/ leaves every core/*.odin mtime alone. No path is hardcoded --
this is GetModuleFileNameW, _NSGetExecutablePath and /proc/self/exe -- so a
package manager's shim still resolves to the real binary. The path feeds the
cache key, so two installations never evict each other, and the mtime feeds the
manifest, so upgrading one in place invalidates what it produced.

Locating the executable is split out of internal_odin_root_dir as
internal_odin_exe_path, so that $ODIN_ROOT no longer suppresses it. $ODIN_ROOT
says where the core collections live, not which compiler is running, and it
still decides the root directory; but it used to short-circuit the lookup
entirely, which left the compiler untracked for everyone who sets it -- distro
packaging, and Odin's own BSD CI jobs. The truncation from an executable path to
its directory was repeated in all three platform lookups and is now written
once.

Environment variables are compared against the allowlist above rather than in
full. PWD, OLDPWD, SHLVL and _ are exported by ordinary shells, so comparing the
whole environment meant a `cd` or a subshell was enough to miss the cache.
Everything omitted affects diagnostics, temporary files, or where the cache
itself lives; Windows finds its toolchain through the registry, not the
environment.

The cache moves out of .odin-cache beside the output into $ODIN_CACHE_DIR, else
%LOCALAPPDATA% on Windows and $XDG_CACHE_HOME/odin or ~/.cache/odin elsewhere,
so it is not committed by accident and is shared between build directories.
`odin clear-cache` empties it, and now does so on POSIX at all:
recursively_delete_directory was Windows-only and returned false everywhere else.

Entries are published through a pid-suffixed temporary and a rename, so racing
compilers cannot hand each other a half-written file, and a truncated executable
can never satisfy a manifest. Manifests are written after the executable they
vouch for. Copies carry the source's permissions across, as gb_file_copy creates
a new file as 0666 on every platform except macOS -- writing over an existing
output used to hide that.

On a hit, an executable that `odin run` or `odin test` is about to delete anyway
is hard-linked rather than copied. macOS validates code signatures per inode, so
a fresh copy pays a full validation on first exec, which is most of what the
cache had just saved. The link keeps the entry's inode while the executable that
runs is still the output path, so a program locating resources beside itself is
unaffected. A persistent output is copied instead: sharing an inode would let the
next build's linker truncate the cache through it. A link that cannot be made,
such as a cache on another filesystem, falls back to copying.

Manifest lines are not written with gb_string_append_fmt, which formats through a
fixed 4096 byte buffer: a longer line, such as a large $PATH, was dropped in
silence, leaving fewer entries in the file than the build produced, so the counts
never matched and the cache missed on every single run. The comparisons now also
require that the manifest and the build agree on how many entries exist, as a
manifest that was a prefix of the current arguments or environment used to pass.

The pre-semantic cache check is skipped when a foreign import is present, for the
same reason #load already skips it: both name files that only the checker
resolves, so gathering there produced a shorter list, hashed to a different cache
directory, and left one behind that was created but never written to.

Known limitation: two builds of the same sources with different flags share a
cache directory and evict one another. They are never handed each other's
executable, only rebuilt.

tests/issues covers the foreign-library rebuild, that build flags and a changed
compiler each invalidate an entry, that a build leaves no temporary files behind
and keeps the restored executable runnable, and that the cache entry survives
`odin run` deleting the executable it was hard-linked to.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants