Description
The compiler cannot figure out that two paths point to the same file when they're symbolic links. It treats them as separate files and requires each path on its own. As a result, a file can be evaluated multiple times which can cause errors about re-opening enums or re-defining constants.
This is easy to reproduce by creating a symlink to a working checkout of this repository:
$ ln -s crystal ../crystal-symlink
$ cd ../crystal-symlink
$ make crystal
Using /usr/bin/llvm-config-14 [version= 14.0.0]
CRYSTAL_CONFIG_BUILD_COMMIT="4ed185ea6" CRYSTAL_CONFIG_PATH='$ORIGIN/../share/crystal/src' SOURCE_DATE_EPOCH="1673965574" CC="cc -fuse-ld=lld" CRYSTAL_CONFIG_LIBRARY_PATH='$ORIGIN/../lib/crystal' ./bin/crystal build -D strict_multi_assign -D preview_overload_order -Dwithout_interpreter -o .build/crystal src/compiler/crystal.cr -D without_openssl -D without_zlib
Showing last frame. Use --error-trace for full trace.
In /home/johannes/src/crystal-lang/crystal/src/compiler/crystal/syntax/ast.cr:120:5
120 | enum Keyword
^
Error: can't reopen enum and add more constants to it
make: *** [Makefile:190: .build/crystal] Fehler 1
$ pwd
/home/johannes/src/crystal-lang/crystal-symlink
$ bin/crystal env CRYSTAL_PATH
lib:/home/johannes/src/crystal-lang/crystal/src
The bin/crystal
wrapper resolves symlinks in SCRIPT_PATH
, thus CRYSTAL_PATH
also points to the target directory instead of the link.
So the compiler looks up non-relative requires (require "foo"
) in the realpath (./crystal/src
), and relative requires (require "./foo"
) in the symlink path (./crystal-symlink/src
).
This is a practical problem when the repository is checked out in a path that has a symlink somewhere. For example, on FreeBSD ~
is /home/username
, but /home
is a symlink to /usr/home/
. If you checkout the Crystal repository in your home path, the compiler won't build, and many specs will also fail.
The immediate confusion is caused by the wrapper script. It resolves the realpath of CRYSTAL_PATH
which is derived from CWD, but does not resolve CWD itself and thus creates a discrepancy.
To fix this, the wrapper script could cd
into CRYSTAL_ROOT
(which is the realpath) before executing the compiler.
I'm also wondering if the compiler should be able to resolve symlinks for discovering whether a file was already required.
I don't see a clear use case for that, except for the bug in the wrapper script which we can fix on its own.
Usually this isn't a problem because you don't require the same source file from both a relative and a non-relative require. This only applies when the file path is reachable through CRYSTAL_PATH
and a relative path. That typically only matches for stdlib/compiler sources.