Add builtins.canonicalPath
Is your feature request related to a problem?
Symlinks in nixpkgs lib.fileset are causing issues: https://github.com/NixOS/nixpkgs/issues/393845 and this function would help resolve it.
Proposed solution
Add a a builtins.canonicalPath with the same behavior as the realpath command, as suggested by @infinisil .
Alternative solutions
Additional context
Possibly somewhat related: https://github.com/NixOS/nix/issues/2109 (but this feature request would only expose a new nix function, not change anything else inside the nixcpp code itself.
Checklist
- [x] checked latest Nix manual (source)
- [x] checked open feature issues and pull requests for possible duplicates
Add :+1: to issues you find important.
Returning a canonical path may not always be feasible. Instead of providing this high-level functionality, perhaps we could just expose enough to more or less match the "file system object" data.
An arbitrary string. Nix does not assign any semantics to symbolic links.
— https://nix.dev/manual/nix/2.28/store/file-system-object
(note that that's in the context of the store layer – the level of abstraction I was thinking of)
Would a builtins.readLink function be sufficient for the purpose of filesets?
It would behave like POSIX readlink, not realpath.
Returning a canonical path may not always be feasible.
Would a
builtins.readLinkfunction be sufficient for the purpose of filesets?
Yes that would be sufficient (should be called builtins.readSymlink imo though) and independently useful, but only because it's possible to implement a canonicalPath based on it:
canonicalPath =
path:
let
split = splitRoot path;
components = subpath.components split.subpath;
canonAppend =
base: name:
let
appended = base + "/${name}";
in
if pathType appended == "symlink" then
lib.foldl' canonAppend base (lib.splitString "/" (readSymlink appended))
else
appended;
recurse =
acc: i:
if i == length components then acc else recurse (canonAppend acc (elemAt components i)) (i + 1);
in
recurse split.root 0;
This isn't as efficient of course, but I think that's fine for a start. Wouldn't mind having a builtins.canonicalPath that does this directly as well, but this can also be done later.
Btw here's a branch where I implemented a working WIP version of a symlink-compatible fileset based on an impure readSymlink: https://github.com/NixOS/nixpkgs/compare/master...tweag:nixpkgs:fileset-symlink
A use case we recently had involved symlinks:
{
lib,
pkgs,
dpLib,
compName,
service,
...
}:
let
compDir = dpLib.component.getRootPath compName;
# Add the OpenAPI files to the docker image.
openapiJSON = pkgs.writeTextDir "openapi3.json" (
builtins.readFile (compDir + "/api/openapi3.json")
);
openapiYAML = pkgs.writeTextDir "openapi3.yaml" (
builtins.readFile (compDir + "/api/openapi3.yaml")
);
in
pkgs.dockerTools.buildLayeredImage {
name = "dac-portal/${service.pname}-service";
tag = service.version;
contents = [
pkgs.cacert
service
# Additional files.
openapiJSON
openapiYAML
];
Here we build a docker image, and we need to go around builtins.readFile because the path is symlink, and there is currently no way to directly move symlinks (with followSymlinks or similar) to the /nix/store.
This workaround is not that nice.
From meeting, we are recommending a builtins.readSymlink as a first step. Currently unsure about the interaction should be between canonicalPath and virtual paths (and related WIP changes).
This pull request has been mentioned on NixOS Discourse. There might be relevant details there:
https://discourse.nixos.org/t/2025-05-04-nix-team-meeting-minutes-230/65206/1