Skip to content

Conversation

dra27
Copy link
Member

@dra27 dra27 commented Sep 15, 2025

This is the first of three PRs which implement Relocatable OCaml as proposed in ocaml/RFCs#53. This PR specifically deals with changes to the handling of ld.conf.

The key changes are:

  • A new configure option --with-stublibs added to address Only set CAML_LD_LIBRARY_PATH for system switches opam-repository#16406 which specifies a custom line to be added to the top of ld.conf. This new flag is intended for use in opam and provides a mechanism to eliminate the setting of CAML_LD_LIBRARY_PATH by the compiler packages in opam-repository.
  • Lines in ld.conf beginning with an explicit reference to the current or parent directory (i.e. ./ or ../) are interpreted relative to the directory containing ld.conf.
  • Previously, if OCAMLLIB (or CAMLLIB) were set in the environment, then the ld.conf was loaded from the directory referred to by this environment variable, and the ld.conf file located in the configured Standard Library location was ignored. The runtime now loads ld.conf from all of these locations, processing them in the same order.

I have presented all of these changes as one PR because while the individual parts of it can be separated, they all alter the same part of the runtime and don't recombine cleanly. While overhauling this infrequently-altered section of the runtime, I propose some further alterations:

  • The processing of ld.conf is made line-ending agnostic - Unix and Cygwin builds now consistently ignore CR characters at the end of each line1
  • Static builds of the compiler no longer create a stublibs directory, nor add it to ld.conf
  • The C implementation of the processing for ld.conf is exposed as a primitive which is now used by ocamlc, instead of having both C and OCaml implementations of the "same" algorithm. My reason for proposing this is twofold: firstly, the two implementations even before this PR are inconsistent (as the special cases in the testsuite reveal) and, secondly, with the other changes proposed here, the two implementations become considerably larger, and thus the benefit of sharing the code is bigger. The commit unifying the two implementations is intentionally towards the end of the series (cf. the deletions in bytecomp/dll.ml in the penultimate commit, versus the deletions in bytecomp/dll.ml for the overall PR)

As far as I know, the only thing apart from OCaml which parses ld.conf is ocamlfind, and the patch for that isn't particularly involved (see, for example, dra27/ocamlfind#7bd58cb). The compiler drivers use the prefix + to refer to the Standard Library (e.g. in -I +unix). Co-opting this notation for ld.conf doesn't work in combination with the change to load ld.conf from all possible locations, since + refers to the effective Standard Library location. Another possibility is to refer to the prefix by a given name, for example, having the default being $/lib/ocaml/stublibs and $/lib/ocaml. Establishing the idea of $ may be useful for other tools; rooting the names of files in the prefix rather than the directory containing the file.

Footnotes

  1. The nefarious user who absolutely must have directories in ld.conf which end with CR characters is able to encode this by including a trailing slash after the name...

dra27 added 9 commits January 15, 2025 14:20
--with-stublibs is intended for use in opam and allows an additional
directory to be added at the top of $(ocamlc -where)/ld.conf.
When calculating the full path for ld.conf, the runtime unconditionally
concatenated "/ld.conf". This is harmless when the separators appear in
the middle of a path ("/usr/local/lib/ocaml//ld.conf" is equivalent to
the version with only single slashes), but it is technically incorrect
for two corners cases with OCAMLLIB and CAMLLIB:

- if either is explicitly set to "/" then "//ld.conf" is _not_ the same
  file as "/ld.conf". This is mildly relevant on Windows and Cygwin
  where the two initial slashes (including as "\/" for native Windows)
  will be interpreted as a UNC path
- if either is explicitly blank, then "ld.conf" (i.e. ld.conf in the
  current directory) is a less illogical file to open than "/ld.conf"
Explicit relative paths in ld.conf are now interpreted relative to the
directory containing ld.conf.
Before, the first ld.conf found from $OCAMLLIB, $CAMLLIB or the
preconfigured standard library location was loaded. Now all of these
are loaded.
The function was only ever added to share the logic between dynlink.c
and startup_byt.c - now that dynlink.c doesn't require it, move the
function to startup_byt.c and make it internal again.
Eliminate the need for two implementations of the parsing logic for
ld.conf by sharing the C implementation (which must exist, since it's
part of bytecode startup) with the bytecode compiler, replacing
Dll.ld_conf_contents
- $libdir/stublibs is no longer created for a --disable-shared build
- When $libdir/stublibs is not created, it is also not added to ld.conf
@dra27 dra27 added run-crosscompiler-tests Makes the CI run the Cross compilers test workflow CI: Full matrix Makes the CI test a bigger set of configurations labels Sep 15, 2025
@dra27 dra27 added the relocatable towards a relocatable compiler label Sep 15, 2025
@dra27 dra27 mentioned this pull request Sep 15, 2025
4 tasks
@nojb nojb self-assigned this Sep 17, 2025
extern char_os * caml_parse_ld_conf(void);
/* If found, parse $OCAMLLIB/ld.conf, $CAMLLIB/ld.conf and stdlib/ld.conf in
that order and add the lines read to table. */
extern char_os * caml_parse_ld_conf(const char_os * stdlib,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI: I did a GitHub search for caml_parse_ld_conf since adding arguments to the argument list of caml_parse_ld_conf is veering into C undefined behavior (if client code accesses an old caml_parse_ld_conf in shared library, or worse if vice versa). No one outside of startup_byt.c is using it, so it looks safe to change.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed, it's guarded by CAML_INTERNALS as well (it's actually only been exposed at all since #9284 in 4.13.0 in order to implement ocamlrun -config ... which was very very early work on Relocatable!)

{
const char_os * stdlib;
stdlib = caml_secure_getenv(T("OCAMLLIB"));
if (stdlib == NULL) stdlib = caml_secure_getenv(T("CAMLLIB"));

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For portable consistency, can we check for NULLs and empty strings? (Unless it is documented somewhere that empty OCAMLLIB stops search for CAMLLIB and OCAML_STDLIB_DIR, which would be weird).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've tackled this in #14247 (the specific bit can be previewed in dra27#227, although this function moves about a bit, so it's in dynlink.c in that PR still!). My rationale was to preserve the "status quo" for in this PR, just because it's not strictly necessary to fix it but, as you can see from the follow-up (and from dra27#225), I firmly agree that we should treat NULL and empty as equivalent!

@jonahbeckford
Copy link

LGTM, with minor comment. I didn't review the test suite since I'm unfamiliar with that.

@dra27
Copy link
Member Author

dra27 commented Sep 29, 2025

Thanks, @jonahbeckford!

I didn't review the test suite since I'm unfamiliar with that

Some more detail for these changes. They're slightly better looked at commit-by-commit:

  • 2e87370 (ld.conf-relative path interpretation): the change in test_ld_conf.ml reflects the new interpretations of some of the lines (e.g. . now being interpreted as libdir)
  • a2e900a (Generate ld.conf using relative paths): this affects testRelocation.ml because ld.conf no longer contains absolute paths. Similarly, testLinkModes.ml is affected because after renaming the directory containing the installed compiler, ld.conf is now correctly processed by the runtime (since the . and ./stublibs entries in it are both still correct)
  • 9062f60 (Load ld.conf from all possible places): this commit alters the runtime to load up to three ld.conf files, instead of one (i.e. $OCAMLLIB/ld.conf, $CAMLLIB/ld.conf and OCAML_STDLIB_DIR/ld.conf are now all loaded, if they exist). That necessarily affects the tests for shadowing in test_ld_conf.ml.
  • 5b339f8 (Harden the parsing of ld.conf w.r.t. load and CRLF): makes the processing of ld.conf consistent w.r.t. CRLF, which consequently affects the tests in test_ld_conf.ml which showed the previous non-portable behaviour
  • 1ddfabc (Use caml_parse_ld_conf in ocamlc): the implementations of the parsing in the dll.ml and dynlink.c differed in various corner cases, all of which were exercised in test_ld_conf.ml - the change therefore is the removal of all the special cases 🥳

@dra27
Copy link
Member Author

dra27 commented Sep 29, 2025

Surfacing a brief discussion on caml-devel about a design-decision affecting this. Non-blank lines in ld.conf are each one of three kinds of path:

  • Absolute (/usr/…, C:\Users\…, \\DC0\Home\…)
  • Explicit-relative (., .., ./… or ../…)
  • Implicit (foo, bar/…, etc.)

In OCaml today, only absolute directories have a sensible semantics, so the core idea in this PR is to add a sensible semantics to either or both of the other two lines (explicit-relative, implicit), on the basis that they're presently not in use.

A complete test for an absolute path is quite difficult to get right on Windows, which is necessarily needed to determine if a line is an implicit path. However, the explicit-relative test is straightforward, which is the main reason I opted just to interpret explicit-relative directories with this new mechanism (it is also the case that opam-bin uses a +dir notation in its patches for ld.conf, and it meant that this would not interfere with that - although that's a relatively minor concern, as I'm sure opam-bin will stop patching the compiler when there's upstream support for relocation anyway).

In this PR right now, we have explicit-relative paths interpreted relative to where ld.conf is found. That means, in opam, the content of the file will be:

../stublibs
./stublibs
.

There are two changes either or both of which could be made. Firstly, the paths could be instead be interpreted relative to --prefix (that requires a little bit of rework to pull in --with-relative-libdir from #14244 or something like it). The file would then be:

./lib/stublibs
./lib/ocaml/stublibs
./lib/ocaml

The second change would be to recognise both explicit-relative and implicit in the same way, allowing

lib/stublibs
lib/ocaml/stublibs
lib/ocaml

The decision in this PR has a small bearing on ocamlfind (but as far as I can tell for nothing else in the ecosystem), as findlib.conf needs to record destdir and path. In my draft for that at dra27/ocamlfind#2, . is being interpreted relative to findlib.conf, giving:

destdir="."
path="./ocaml:."

but which could therefore be:

destdir="./lib" # Or just destdir="lib"
path="./lib/ocaml:./lib" # Or just path="lib/ocaml:lib"

bytecomp/dll.ml Outdated
if line = "" then
""
else
let len = String.length line in
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've tried to make the logic easier to understand. I hope I've managed to preserve all aspect.

if Filename.is_relative line && not (Filename.is_implicit line)
then
  match line with
  | "." -> Config.standard_library
  | _ -> Filename.concat Config.standard_library line
else line

Copy link
Member Author

@dra27 dra27 Sep 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(note that this function gets completely deleted in 1ddfabc, where Dll co-opts the C implementation).

It's different for line = "./stublibs", I think - the previous version (and the version in C) produces <libdir>/stublibs, but the newer version produces <libdir>/./stublibs. It's pedantic, but I was trying where possible to avoid the stray /./ bits being introduced, given that the default entries are . and ./stublibs!

@jonahbeckford
Copy link

Some more detail for these changes. They're slightly better looked at commit-by-commit:
...

Thanks, that was helpful for me to review the testsuite. It all looks good to me.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
CI: Full matrix Makes the CI test a bigger set of configurations relocatable towards a relocatable compiler run-crosscompiler-tests Makes the CI run the Cross compilers test workflow
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants