Skip to content

Instantly share code, notes, and snippets.

@jlesquembre
Created October 6, 2023 22:31
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jlesquembre/ad7cb8357fe13312eb23a437b87bcd2b to your computer and use it in GitHub Desktop.
Save jlesquembre/ad7cb8357fe13312eb23a437b87bcd2b to your computer and use it in GitHub Desktop.
JDK and GraalVM 'user.home' in Nix

Clojure CLI, the bash script, determines the user config directory looking at the CLJ_CONFIG env variable. If unset, fallbacks to $HOME/.clojure, and creates a default deps.edn file if you don't have one:

# Determine user config directory
if [[ -n "$CLJ_CONFIG" ]]; then
  config_dir="$CLJ_CONFIG"
elif [[ -n "$XDG_CONFIG_HOME" ]]; then
  config_dir="$XDG_CONFIG_HOME/clojure"
else
  config_dir="$HOME/.clojure"
fi

# If user config directory does not exist, create it
if [[ ! -d "$config_dir" ]]; then
  mkdir -p "$config_dir"
fi
if [[ ! -e "$config_dir/deps.edn" ]]; then
  cp "$install_dir/example-deps.edn" "$config_dir/deps.edn"
fi
if [ "$install_dir/tools.edn" -nt "$config_dir/tools/tools.edn" ]; then
  mkdir -p "$config_dir/tools"
  cp "$install_dir/tools.edn" "$config_dir/tools/tools.edn"
fi

source

Babashka mimics that behaviour:

(defn get-config-dir
  "Retrieves configuration directory.
  First tries `CLJ_CONFIG` env var, then `$XDG_CONFIG_HOME/clojure`, then ~/.clojure."
  []
  (or (*getenv-fn* "CLJ_CONFIG")
      (when-let [xdg-config-home (*getenv-fn* "XDG_CONFIG_HOME")]
        (.getPath (io/file xdg-config-home "clojure")))
      (.getPath (io/file (home-dir) ".clojure"))))

source

But there is a subtle difference, Babashka doesn't take the $HOME value, but (System/getProperty "user.home"):

(defn- home-dir []
  (if windows?
    ;; workaround for https://github.com/oracle/graal/issues/1630
    (*getenv-fn* "userprofile")
    (System/getProperty "user.home")))

source

Why is that difference? I guess because the Java version, at least in theory, is system independent (it works on Windows). Or maybe because Clojure code base also invokes (System/getProperty "user.home"), and Babashka mimics Clojure:

(defn user-deps-path
  "Use the same logic as clj to calculate the location of the user deps.edn.
  Note that it's possible no file may exist at this location."
  []
  (let [config-env (System/getenv "CLJ_CONFIG")
        xdg-env (System/getenv "XDG_CONFIG_HOME")
        home (System/getProperty "user.home")
        config-dir (cond config-env config-env
                         xdg-env (str xdg-env File/separator "clojure")
                         :else (str home File/separator ".clojure"))]
    (str config-dir File/separator "deps.edn")))

source

(defn ^:private local-repo-path
  "Helper to form the path to the default local repo - use `@cached-local-repo` for
  caching delayed value"
  []
  (.getAbsolutePath (jio/file (System/getProperty "user.home") ".m2" "repository")))

source

And I guess Clojure does it because the call is system independent. Or maybe to mimic Maven, I don't really know.

Now the question is, why in Java System.getProperty("user.home"); doesn't return the same value as $HOME? And why does it return a different value in Linux vs MacOS?

If we looking at JDK source code, we can see that getProperty property is implemented in C. On Linux, does a call to getpwuid(), which gets the HOME directory from /etc/passwd:

    /* user properties */
    {
        struct passwd *pwent = getpwuid(getuid());
        sprops.user_name = pwent ? strdup(pwent->pw_name) : "?";
#ifdef MACOSX
        setUserHome(&sprops);
#else
        sprops.user_home = pwent ? strdup(pwent->pw_dir) : NULL;
#endif
        if (sprops.user_home == NULL || sprops.user_home[0] == '\0' ||
            sprops.user_home[1] == '\0') {
            // If the OS supplied home directory is not defined or less than two characters long
            // $HOME is the backup source for the home directory, if defined
            char* user_home = getenv("HOME");
            if ((user_home != NULL) && (user_home[0] != '\0')) {
                sprops.user_home = user_home;
            } else {
                sprops.user_home = "?";
            }

source

In the Nix sandbox environment, in the /etc/passwd, the user home is set to /build, which is writable.

But on MacOS, JDK invokes NSHomeDirectory() to find the user home directory:

/*
 * Method for fetching the user.home path and storing it in the property list.
 * For signed .apps running in the Mac App Sandbox, user.home is set to the
 * app's sandbox container.
 */
void setUserHome(java_props_t *sprops) {
    if (sprops == NULL) { return; }
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
    sprops->user_home = createUTF8CString((CFStringRef)NSHomeDirectory());
    [pool drain];
}

source

Which returns /var/empty in the Nix sandbox, and the sandbox user doesn't have access to that directory. And Babashka fails to create the default deps.edn file.

One last thing to mention, GraalVM System.getProperty implementation is different from OpenJDK, but as you can see, GraalVM mimics JDK:

    /*
     * Initialization code is adapted from the JDK native code that initializes the system
     * properties, as found in src/solaris/native/java/lang/java_props_md.c
     */

    @Override
    protected String userNameValue() {
        Pwd.passwd pwent = Pwd.getpwuid(Unistd.getuid());
        return pwent.isNull() ? "?" : CTypeConversion.toJavaString(pwent.pw_name());
    }

    @Override
    protected String userHomeValue() {
        Pwd.passwd pwent = Pwd.getpwuid(Unistd.getuid());
        return pwent.isNull() ? "?" : CTypeConversion.toJavaString(pwent.pw_dir());
    }

source

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment