about summary refs log tree commit diff
path: root/nix/sparseTree/default.nix
# Build a “sparse” version of a given directory, only including contained files
# and directories if they are listed in a supplied list:
#
# # A very minimal depot
# sparseTree {
#   root = ./depot;
#   paths = [
#     ./default.nix
#     ./depot/nix/readTree/default.nix
#     ./third_party/nixpkgs
#     ./third_party/overlays
#   ];
# }
{ pkgs, lib, ... }:

{
  # root path to use as a reference point
  root
, # list of paths below `root` that should be
  # included in the resulting directory
  #
  # If path, need to refer to the actual file / directory to be included.
  # If a string, it is treated as a string relative to the root.
  paths
, # (optional) name to use for the derivation
  #
  # This should always be set when using roots that do not have
  # controlled names, such as when passing the top-level of a git
  # repository (e.g. `depot.path.origSrc`).
  name ? builtins.baseNameOf root
}:

let
  rootLength = builtins.stringLength (toString root);

  # Count slashes in a path.
  #
  # Type: path -> int
  depth = path: lib.pipe path [
    toString
    (builtins.split "/")
    (builtins.filter builtins.isList)
    builtins.length
  ];

  # (Parent) directories will be created from deepest to shallowest
  # which should mean no conflicts are caused unless both a child
  # and its parent directory are in the list of paths.
  # TODO(sterni): improve error messages in such cases
  fromDeepest = lib.sort (a: b: depth a < depth b) paths;

  # Create a set which contains the source path to copy / symlink and
  # it's destination, so the path below the destination root including
  # a leading slash. Additionally some sanity checking is done.
  makeSymlink = path:
    let
      withLeading = p: if builtins.substring 0 1 p == "/" then p else "/" + p;
      fullPath =
        if builtins.isPath path then path
        else if builtins.isString path then (root + withLeading path)
        else builtins.throw "Unsupported path type ${builtins.typeOf path}";
      strPath = toString fullPath;
      contextPath = "${fullPath}";
      belowRoot = builtins.substring rootLength (-1) strPath;
      prefix = builtins.substring 0 rootLength strPath;
    in
    assert toString root == prefix; {
      src = contextPath;
      dst = belowRoot;
    };

  symlinks = builtins.map makeSymlink fromDeepest;
in

# TODO(sterni): teach readTree to also read symlinked directories,
  # so we ln -sT instead of cp -aT.
pkgs.runCommand "sparse-${name}" { } (
  lib.concatMapStrings
    ({ src, dst }: ''
      mkdir -p "$(dirname "$out${dst}")"
      cp -aT --reflink=auto "${src}" "$out${dst}"
    '')
    symlinks
)