Symbolic links to source files and finding relative files?

This topic follows Running a script via a symlink in a different directory fails to load module files · Issue #1201 · SWI-Prolog/swipl-devel · GitHub

I think there are two related questions:

  • When using swipl file ... and file is a symbolic link to a *.pl file, should this be handled as swipl link-target .... This would allow creating an application in some directory and make it available as an executable by creating a symlink with the desired executable name to the main .pl file in the application directory.

  • How should we resolve relative file paths by use_module/1, etc. if we load a file through a symbolic link? As is, we only look in the directory that holds the link. Should we also look in the directory where the link point at? And, if so, in all possible additional directories if we have a chain of symbolic links? In what order should these be checked?

  • Does it make a difference of whether the file name of the link is the same as the file name of the link target or not?

The OP points at Python, that at least seems the handle the first case. What do other languages do?

I think Python simply uses PYTHONHOME and PYTHONPATH to find imports, so it doesn’t matter where the Python executable (see the documentation for how these default).

For more control, there’s the importlib, which is rather overwhelming in its many possibilities (and which I don’t pretend to understand).

If I understand the question correctly, the problem is that SWI-Prolog’s library search uses the flag home and it’s not clear what to do with this if it’s a symbolic link? (as documented for file_search_path/2)

No. Please look at the github issue. The situation is that we have a directory D with files f1.pl and f2.pl where f1.pl uses :- use_module(f2). From anywhere, we can run swipl /path/to/D/f1.pl and this will work fine because relative file names are searched relative to the file currently being loaded first and, next, relative to the process working directory.

If, however, we use #!/usr/bin/env swipl in f1.pl and now create a symlink from ~/bin/f1 to /path/to/D/f1.pl, running f1 complains that f2.pl does not exist. A similar scenario with Python is claimed to work (not tested myself).

So, the question is which rules about symlink handling make this work for Python, how do other languages handle this and what should we do (including the option to do nothing :slight_smile: )

Or maybe you need options like -H and -L in the Unix commands file, ls, find. :slight_smile:
[which are inconsistently named in those three commands.]

My general feeling with Prolog “relative includes” is that it got most things right, at least compared to “includes” in C and Python (e.g., C’s #include <foo> vs #include "foo", or the baroqueness of Python “packages”, “modules”, and __init__.py). But there are many strange corner cases that I’ve never explored, such as symlinks, and PYTHONPATH with . as one of the items.

5 posts were split to a new topic: Symbolic links and real file names

From the GitHub ticket its seems that the OP is invoking main.pl
with she bang (#!) and not manually with command line option of
swipl. For example from JavaScript I know that import.meta.url and

process.argv[1] have different values. Usually import.meta.url
shows the real path, and process.argv[1] shows the command
line input. So yes, there are programming languages which

compute real paths for their modules, demonstrably for scripts passed
via she bang (#!). JavaScript is among them, here an example, using the
Windows command line, the command line that a she bang (#!) generates:

/* nodeJS called with a file symbolic link */
>node.exe <dir>\foo\jsdog.mjs

And this is what one can observe inside the .mjs file:

process.argv[1]=<dir>\foo\jsdog.mjs
import.meta.url=<dir2>\player\drawer\index.mjs

Edit 14.10.2023
Please be patient, managed only to investigate JavaScript. For Python
one has for example to check what is the custom for __file__, is it a
real path or not? One could then figure out whether there are more

programming languages that compute a real path for their modules,
especially in the she bang (#!) launch phase.

Does SWI-Prolog use its executable location for finding the library? If so, wouldn’t it be better if the installation process stored the location in the executable (or in libswipl.so), rather than computing this dynamically?

There probably is no “best” answer to converting paths with symlinks to absolute paths because of conflicting requirements:

  • open /path/to/symlink_to_foo should follow the symlink.
  • rm /path/to/symlink_to_foo should not follow the symlink (an early version of BSD Unix got this wrong; fortunately I had backups)
  • should /path/to/symlink_to_dir/another_dir/../foo follow the symlink or not?

It’s not clear that Python got this right … the Google style guide for Python forbids relative links (from . import foo can result in double imports, it seems; and from .. import foo is even more problematic). So, SWI-Prolog’s attempt to keep things canonical seems to be better than Python. (Python has the os.path module that provides provides normpath, realpath, samefile, samestat, etc. and attempts to handle both Unix and Windows)

Maybe we can learn something from Plan9: https://9p.io/sys/doc/lexnames.html … after some iterations, this is the definition for the meaning of .. that they settled on:

The parent of a directory X, X/.., is the same directory that would obtain if we instead accessed the directory named by stripping away the last path name element of X.
For example, if we are in the directory /a/b/c and chdir to .., the result is exactly as if we had executed a chdir to /a/b.
This definition is easy to understand and seems natural. It is, however, a purely lexical definition that flatly ignores evaluated file names, mount tables, and other kernel-resident data structures.

Actually I cannot reproduce what the OP reported about Python:

And I create a symlink from ~/bin/main to ~/src/hello/main.py.

On my side, on windows platform, a file symbolic link main text alone
works for JavaScript, but it does not work for Python:

/* CPython called with a file symbolic link, on Windows */
>python.exe <dir>\foo\pydog.py

Gives me an error, the first import that the main text does fails:

ModuleNotFoundError: No module named 'store'

Python usually automatically adds the directory of the main text to the search path,
but it fails to do so correctly for a file symbolic link. So how did the OP run his
example? Mostlilely he had the variable PYTHONPATH also configured.

Adding the resolved path to PYTHONPATH indeed helps:

set PYTHONPATH=<dir2>\playerpy\drawer

After this additional step, the file symbolic link works. Or maybe it nevertheless
worked for the OP, because the operating system she bang (#!) mechanism
did some real path resolution?

See SWI-Prolog -- The Prolog‘home' directory

This is unrelated though. The issue is whether or not file search relative to a source file being loaded should be affected on whether or not the source file is a symbolic link. And, if so, whether this applies only to .pl files that are passed as command line arguments or to any file being loaded.

I don’t think the latter is a good idea. If you have a project and you want to add some other project (whose sources are a directory hierarchy), you can either symlink the directory holding the project or you add a a clause to file_search_path/2 to find the dependency and use that to load it. Symlinking an individual file and hoping to pull in the whole project seems dubious to me.

From the command line arguments it makes more sense to me. But, I’m also inclined to believe that it causes more confusion that it is worth. Python seems to do it. That is an argument in favor as there is experience and it caused the OP to expect this to work in Prolog as well.

Possibly not on Windows, but on Linux it seems to work as claimed. Given

  foo
    f1.py
    f2.py
  bar
    f1.py -> ../foo/f1.py

with f1.py holding

import f2
f2.hello()

and f2.py holding

def hello():
    print("Hello World")

I can run python f1.py in bar and it works just fine. It indeed only seems to work for the main file. If I create bar/f0.pythat importsf1.py(the link) it indeed complainsf2` does not exist.