Skip to content

Pin LC_COLLATE for file ordering, as pkgload does - #16

Merged
felix-andreas-finccam merged 1 commit into
mainfrom
fix/locale-independent-file-order
Sep 22, 2026
Merged

felix-andreas-finccam merged 1 commit into
mainfrom
fix/locale-independent-file-order

Conversation

@felix-andreas-finccam

Copy link
Copy Markdown
Contributor

The problem

load_fast() sorts the files in R/ before it loads them. It used order() with no options. order() sorts text using the locale of the current R session.

On Linux the usual locale is en_US.UTF-8. This locale ignores _ and . when it compares names. Two files that differ only in those characters can therefore load in a different order than R itself would use.

This breaks any package that needs a fixed load order. A file can run code at the top level. If that code calls a function from another file, the other file must load first.

We hit this in a package with 564 files. 405 of them changed position. Two files then failed:

  • utils_log_instances.R loaded before utils_log.R
  • portfolio_saa_multiasset.R loaded before portfolio.R

Each of those files calls a function at the top level. The function did not exist yet, so the load stopped with "could not find function".

The fix

Two other R loaders already solve this. Both pin the sort order to the "C" locale:

  • R CMD INSTALL sets lccollate <- "C" in tools:::.install_package_code_files
  • devtools::load_all() wraps its file listing in withr::local_collate("C") inside find_code()

This change does the same. A small helper sets LC_COLLATE to "C", runs the sort, then puts the old value back.

The helper covers the sorting only. Package code still runs under the user's own locale. This is how devtools::load_all() behaves, and it is why the helper does not simply wrap the whole function.

loadfast depends only on rlang, so the helper uses base R rather than adding withr.

Tests

Stage 3d2 is new. It builds a package with two files. utils_log.R defines a function. utils_log_instances.R calls that function at the top level. The test sets the locale to en_US.UTF-8 and loads the package.

The test checks three things:

  1. The load succeeds.
  2. The top-level call found the function.
  3. The caller's LC_COLLATE is unchanged after the load.

Check 3 matters because this fix changes global state. The test skips itself if en_US.UTF-8 is not installed.

Test results:

  • Without the fix: 259 passed, 2 failed.
  • With the fix: 262 passed, 0 failed.

I also ran the real 564-file package. It now loads under en_US.UTF-8 with no locale override, and the session locale is the same before and after.

🤖 Generated with Claude Code

load_fast() sorted R/ files with a plain order(basename(...)), which follows
the session's LC_COLLATE. glibc's en_US.UTF-8 ignores "_" and "." at the
primary level, so files differing only in those characters came out in a
different order than R CMD INSTALL produces -- and a package whose top-level
code runs in install order then fails to load.

On a 564-file package, 405 of the files changed position versus the C order.
Two concrete breakages: utils_log_instances.R sorted ahead of utils_log.R,
and portfolio_saa_multiasset.R ahead of portfolio.R, each with a top-level
call to a function the earlier-sorting file had not defined yet.

Mirror pkgload: find_code() wraps its listing in withr::local_collate("C"),
and R CMD INSTALL pins lccollate <- "C" in
tools:::.install_package_code_files. The pin covers the ordering only, not the
sourcing, so package code still runs under the user's locale -- and the
caller's LC_COLLATE is restored on exit, which the new test asserts.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@felix-andreas-finccam
felix-andreas-finccam force-pushed the fix/locale-independent-file-order branch from 3f09c7e to f51cde9 Compare September 22, 2026 12:50
@felix-andreas-finccam
felix-andreas-finccam merged commit c86a845 into main Sep 22, 2026
7 checks passed
@felix-andreas-finccam
felix-andreas-finccam deleted the fix/locale-independent-file-order branch September 22, 2026 13:33
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.

1 participant