Pass COURSIER_CACHE to coursier when fetching unpinned repositories (#1601)
Coursier derives its default cache location from the JVM's `user.home`,
which on Linux comes from the passwd database and ignores $HOME, while
these rules derive the shared cache location from $HOME. When the two
disagree (e.g. $HOME remapped to a network mount), repinning bypasses
the shared cache and fails with "Error while trying to parse the path
of file in the coursier cache".
Pass the computed location to the spawned coursier process via
COURSIER_CACHE, as the pinned flow already does. If no variable a cache
location can be derived from is set, leave the environment untouched.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
diff --git a/private/rules/coursier.bzl b/private/rules/coursier.bzl
index 3419af1..8f20e49 100644
--- a/private/rules/coursier.bzl
+++ b/private/rules/coursier.bzl
@@ -1008,6 +1008,34 @@
return default_cache_dir
+# Compute the environment for the spawned coursier process, forcing the cache
+# location that get_coursier_cache_or_default computed.
+#
+# Coursier itself would otherwise derive its default cache location from the
+# JVM's `user.home` property, which on Linux comes from the passwd database and
+# ignores $HOME, while this file derives it from $HOME (or $XDG_CACHE_HOME). If
+# the two disagree (e.g. $HOME points at a network mount), coursier would
+# bypass the shared cache and report artifact paths that cannot be relativized
+# against it. Setting COURSIER_CACHE explicitly keeps both sides in agreement.
+# For repositories that do not use the shared cache, this also prevents
+# coursier from writing into home directories.
+# https://github.com/bazelbuild/rules_jvm_external/issues/301
+# https://github.com/coursier/coursier/blob/1cbbf39b88ee88944a8d892789680cdb15be4714/modules/paths/src/main/java/coursier/paths/CoursierPaths.java#L29-L56
+#
+# This method is public for testing.
+def get_coursier_environment(repository_ctx, use_unsafe_shared_cache):
+ if use_unsafe_shared_cache and not _is_windows(repository_ctx):
+ # Every shared cache location on this platform is derived from one of
+ # these variables. If none of them is usable, forcing COURSIER_CACHE
+ # would point coursier at a nonsensical path; leave the environment
+ # alone and let coursier use its own default instead.
+ os_env = repository_ctx.os.environ
+ xdg_cache_home = "" if _is_macos(repository_ctx) else os_env.get("XDG_CACHE_HOME", "")
+ if not (os_env.get("COURSIER_CACHE", "") or xdg_cache_home or os_env.get("HOME", "")):
+ return {}
+ cache_location = get_coursier_cache_or_default(repository_ctx, use_unsafe_shared_cache)
+ return {"COURSIER_CACHE": str(repository_ctx.path(cache_location))}
+
def make_coursier_dep_tree(
repository_ctx,
artifacts,
@@ -1103,19 +1131,14 @@
cmd.append("--javadoc")
cmd.append("--default=true")
- environment = {}
- if not _is_unpinned(repository_ctx):
+ is_unpinned = _is_unpinned(repository_ctx)
+ if not is_unpinned:
coursier_cache_location = get_coursier_cache_or_default(
repository_ctx,
False,
)
cmd.extend(["--cache", coursier_cache_location]) # Download into $output_base/external/$maven_repo_name/v1
-
- # If not using the shared cache and the user did not specify a COURSIER_CACHE, set the default
- # value to prevent Coursier from writing into home directories.
- # https://github.com/bazelbuild/rules_jvm_external/issues/301
- # https://github.com/coursier/coursier/blob/1cbbf39b88ee88944a8d892789680cdb15be4714/modules/paths/src/main/java/coursier/paths/CoursierPaths.java#L29-L56
- environment = {"COURSIER_CACHE": str(repository_ctx.path(coursier_cache_location))}
+ environment = get_coursier_environment(repository_ctx, is_unpinned)
cmd.extend(additional_coursier_options)
diff --git a/tests/unit/coursier_test.bzl b/tests/unit/coursier_test.bzl
index 3cfecc3..a542aaa 100644
--- a/tests/unit/coursier_test.bzl
+++ b/tests/unit/coursier_test.bzl
@@ -4,6 +4,7 @@
"//private/rules:coursier.bzl",
"compute_dependency_inputs_signature",
"get_coursier_cache_or_default",
+ "get_coursier_environment",
"get_coursier_sha256",
"get_direct_dependencies",
"get_netrc_lines_from_entries",
@@ -448,6 +449,118 @@
get_coursier_cache_or_default_enabled_with_custom_location_test = add_test(_get_coursier_cache_or_default_enabled_with_custom_location_test)
+def _get_coursier_environment_without_shared_cache_test(ctx):
+ env = unittest.begin(ctx)
+ mock_repository_ctx = struct(
+ os = struct(
+ environ = {
+ "HOME": "/home/testuser",
+ },
+ name = "linux",
+ ),
+ which = _mock_which,
+ path = _mock_repo_path,
+ )
+ asserts.equals(
+ env,
+ {"COURSIER_CACHE": "/mockroot/v1"},
+ get_coursier_environment(mock_repository_ctx, False),
+ )
+ return unittest.end(env)
+
+get_coursier_environment_without_shared_cache_test = add_test(_get_coursier_environment_without_shared_cache_test)
+
+# Regression test for the shared cache with $HOME pointing somewhere the JVM's
+# `user.home` does not (e.g. a network mount): the spawned coursier process
+# must be told to use the $HOME-derived cache location explicitly, otherwise it
+# falls back to `user.home` and bypasses the shared cache.
+def _get_coursier_environment_with_shared_cache_uses_home_test(ctx):
+ env = unittest.begin(ctx)
+ mock_repository_ctx = struct(
+ os = struct(
+ environ = {
+ "HOME": "/mnt/nas/testuser",
+ },
+ name = "linux",
+ ),
+ which = _mock_which,
+ path = _mock_repo_path,
+ )
+ asserts.equals(
+ env,
+ {"COURSIER_CACHE": "/mnt/nas/testuser/.cache/coursier/v1"},
+ get_coursier_environment(mock_repository_ctx, True),
+ )
+ return unittest.end(env)
+
+get_coursier_environment_with_shared_cache_uses_home_test = add_test(_get_coursier_environment_with_shared_cache_uses_home_test)
+
+def _get_coursier_environment_with_custom_coursier_cache_test(ctx):
+ env = unittest.begin(ctx)
+ mock_repository_ctx = struct(
+ os = struct(
+ environ = {
+ "COURSIER_CACHE": "/custom/location",
+ "HOME": "/home/testuser",
+ },
+ name = "linux",
+ ),
+ which = _mock_which,
+ path = _mock_repo_path,
+ )
+ asserts.equals(
+ env,
+ {"COURSIER_CACHE": "/custom/location"},
+ get_coursier_environment(mock_repository_ctx, True),
+ )
+ return unittest.end(env)
+
+get_coursier_environment_with_custom_coursier_cache_test = add_test(_get_coursier_environment_with_custom_coursier_cache_test)
+
+def _get_coursier_environment_with_xdg_cache_home_test(ctx):
+ env = unittest.begin(ctx)
+ mock_repository_ctx = struct(
+ os = struct(
+ environ = {
+ "HOME": "/home/testuser",
+ "XDG_CACHE_HOME": "/xdg/cache",
+ },
+ name = "linux",
+ ),
+ which = _mock_which,
+ path = _mock_repo_path,
+ )
+ asserts.equals(
+ env,
+ {"COURSIER_CACHE": "/xdg/cache/coursier/v1"},
+ get_coursier_environment(mock_repository_ctx, True),
+ )
+ return unittest.end(env)
+
+get_coursier_environment_with_xdg_cache_home_test = add_test(_get_coursier_environment_with_xdg_cache_home_test)
+
+# Without $HOME (or any other variable a shared cache location could be derived
+# from), coursier must be left to use its own default rather than being pointed
+# at a nonsensical path.
+def _get_coursier_environment_without_home_test(ctx):
+ env = unittest.begin(ctx)
+ mock_repository_ctx = struct(
+ os = struct(
+ environ = {},
+ name = "linux",
+ ),
+ which = _mock_which,
+ path = _mock_repo_path,
+ )
+ asserts.equals(
+ env,
+ {},
+ get_coursier_environment(mock_repository_ctx, True),
+ )
+ return unittest.end(env)
+
+get_coursier_environment_without_home_test = add_test(_get_coursier_environment_without_home_test)
+
def _get_coursier_sha256_default_test_impl(ctx):
env = unittest.begin(ctx)
asserts.equals(