diff --git a/lib/filesystem.nix b/lib/filesystem.nix index 0a1275e547cf2..63bfede9a7833 100644 --- a/lib/filesystem.nix +++ b/lib/filesystem.nix @@ -1,4 +1,22 @@ { lib }: + +let + inherit (lib.attrsets) + mapAttrs + ; + inherit (lib.lists) + head + tail + ; + inherit (lib.strings) + hasPrefix + ; + inherit (lib.filesystem) + pathHasPrefix + absolutePathComponentsBetween + ; +in + { # haskellPathsInDir : Path -> Map String Path # A map of all haskell packages defined in the given path, # identified by having a cabal file with the same name as the @@ -54,4 +72,108 @@ dir + "/${name}" ) (builtins.readDir dir)); + # pathHasPrefix : Path -> Path -> Bool + # pathHasPrefix ancestor somePath + # + # Return true iff, disregarding trailing slashes, + # - somePath is below ancestor + # - or equal to ancestor + # + # Equivalently, return true iff you can reach ancestor by starting at somePath + # and traversing the `..` node zero or more times. + pathHasPrefix = prefixPath: otherPath: + let + normalizedPathString = pathLike: toString (/. + pathLike); + pre = normalizedPathString prefixPath; + other = normalizedPathString otherPath; + in + if pre == "/" # root is the only path that already ends in "/" + then true + else + hasPrefix (pre + "/") (other + "/"); + + + # commonPath : PathLike -> PathLike -> Path + # + # Find the common ancestory; the longest prefix path that is common between + # the input paths. + commonPath = a: b: + let + b' = /. + b; + go = c: + if pathHasPrefix c b' + then c + else go (dirOf c); + in + go (/. + a); + + # absolutePathComponentsBetween : PathOrString -> PathOrString -> [String] + # absolutePathComponentsBetween ancestor descendant + # + # Returns the path components that form the path from ancestor to descendant. + # Will not return ".." components, which is a feature. Throws when ancestor + # and descendant arguments aren't in said relation to each other. + # + # Example: + # + # absolutePathComponentsBetween /a /a/b/c == ["b" "c"] + # absolutePathComponentsBetween /a/b/c /a/b/c == [] + absolutePathComponentsBetween = + ancestor: descendant: + let + a' = /. + ancestor; + go = d: + if a' == d + then [] + else if d == /. + then throw "absolutePathComponentsBetween: path ${toString ancestor} is not an ancestor of ${toString descendant}" + else go (dirOf d) ++ [(baseNameOf d)]; + in + go (/. + descendant); + + /* + Memoize a function that takes a path argument. + + Example: + + analyzeTree = dir: + let g = memoizePathFunction (p: t: expensiveFunction p) (p: {}) dir; + in presentExpensiveData g; + + Type: + memoizePathFunction :: (Path -> Type -> a) -> (Path -> a) -> Path -> (Path -> a) + */ + memoizePathFunction = + # Function to memoize + f: + # What to return when a path does not exist, as a function of the path + missing: + # Filesystem location below which the returned function is defined. `/.` may be acceptable, but a path closer to the data of interest is better. + root: + let + makeTree = dir: type: { + value = f dir type; + children = + if type == "directory" + then mapAttrs + (key: type: makeTree (dir + "/${key}") type) + (builtins.readDir dir) + else {}; + }; + + # This is where the memoization happens + tree = makeTree root (lib.pathType root); + + lookup = notFound: list: subtree: + if list == [] + then subtree.value + else if subtree.children ? ${head list} + then lookup notFound (tail list) subtree.children.${head list} + else notFound; + in + path: lookup + (missing path) + (absolutePathComponentsBetween root path) + tree; + } diff --git a/lib/sources.nix b/lib/sources.nix index 407829b547b08..045fc9644b3e9 100644 --- a/lib/sources.nix +++ b/lib/sources.nix @@ -10,14 +10,31 @@ let split storeDir tryEval + typeOf ; inherit (lib) + attrByPath boolToString + concatMapStrings + concatStringsSep filter getAttr + head isString + mapAttrs pathExists readFile + tail + ; + inherit (lib.strings) + sanitizeDerivationName + ; + inherit (lib.filesystem) + commonPath + memoizePathFunction + pathHasPrefix + pathIntersects + absolutePathComponentsBetween ; # Returns the type of a path: regular (for file), symlink, or directory @@ -76,6 +93,9 @@ let # Optional with default value: constant true (include everything) # The function will be combined with the && operator such # that src.filter is called lazily. + # The entire store path is filtered, including the files that are + # accessible via relative path from the subpath focused on by + # `focusAt` or `union`. # For implementing a filter, see # https://nixos.org/nix/manual/#builtin-filterSource # @@ -89,7 +109,212 @@ let inherit (orig) origSrc; filter = path: type: filter path type && orig.filter path type; name = if name != null then name else orig.name; + subpath = orig.subpath; }; + _filter = fn: src: cleanSourceWith { filter = fn; inherit src; }; + + /* + Produce a source that contains all the files in `base` and `extras` and + points at the location of `base`. The returned source will be a reference + to a subpath of a store path when it is necessary to accomodate for the + relative locations of `extras`. + + When used in the `stdenv` `src` parameter, the whole source will be copied + and the build script will `cd` into the path that corresponds to `base`. + + The result will match the first argument with respect to the name and + original location of the subpath (see `sources.focusAt`). + + Type: + extend : SourceLike -> [SourceLike] -> Source + */ + extend = + # Source-like object that serves as the starting point. The path `"${extend base extras}"` points to the same file as `"${base}"` + baseRaw: + # List of sources that will also be included in the store path. + extrasRaw: + let + baseAttrs = toSourceAttributes baseRaw; + extrasAttrs = map toSourceAttributes extrasRaw; + root = lib.foldl' (a: b: commonPath a b.origSrc) baseAttrs.origSrc extrasAttrs; + sourcesIncludingPath = + memoizePathFunction + (path: type: + if path == root + then [ baseAttrs ] ++ extrasAttrs + else filter + (attr: pathHasPrefix path attr.origSrc # path leads up to origSrc + || (pathHasPrefix attr.origSrc path && attr.filter path type)) + (sourcesIncludingPath (dirOf path)) + ) + (path: throw "sources.extend: path does not exist: ${toString path}") + root; + in + fromSourceAttributes { + origSrc = root; + filter = path: type: sourcesIncludingPath path != []; + name = baseAttrs.name; + subpath = absolutePathComponentsBetween root baseAttrs.origSrc ++ baseAttrs.subpath; + }; + + /* + Almost the identity of sources.extend when it comes to the filter function; + `extend` will always include the nodes that lead up to `path`. + + Type: + empty :: Path -> Source + */ + empty = path: cleanSourceWith { src = path; filter = _: _: false; }; + + /* + Produce a new source identical to `source` except its string interpolation + (or `outPath`) resolves to a subpath that corresponds to `path`. + + When used in the `stdenv` `src` parameter, the whole of `source` will be + copied and the build script will `cd` into the path that corresponds to `path`. + + Type: + focusAt :: Path -> Source -> Source + + Example: + # suppose we have files ./foo.json and ./bar/default.json + src = sources.filter (path: type: type == "directory" || hasSuffix ".json" path) ./. + "${src}/bar/default.json" + => "/nix/store/pjn...-source/bar/default.json" + # ^ exists + + "${sources.focusAt ./bar src}/../foo.json" + => "/nix/store/pjn...-source/bar/../foo.json" + # ^ exists; notice the store hash is the same + + # contrast with cutAt + "${sources.cutAt ./bar src}/../foo.json" + => "/nix/store/ls9...-source/../foo.json" + # ^ does not exist (resolves to /nix/store/foo.json) + */ + focusAt = path: srcRaw: + assert typeOf path == "path"; + let + orig = toSourceAttributes srcRaw; + valid = + if ! pathHasPrefix orig.origSrc path + then throw "sources.focusAt: new path is must be a subpath (or self) of the original source directory. But ${toString path} is not a subpath (or self) of ${toString orig.origSrc}" + else if ! pathExists path + then + # We could provide a function that allows this, but it seems to be a + # bad idea unless we encounter a _good_ use case. + throw "sources.focusAt: new path ${toString path} does not exist on the filesystem and would point to a file that doesn't exist in the store. This is usually a mistake." + else if ! orig.filter path (pathType path) + then + throw "sources.focusAt: new path ${toString path} is not actually included in the source. Potential causes include an incorrect path, incorrect filter function or a forgotten sources.extend call." + else x: x; + in + valid (fromSourceAttributes (orig // { + subpath = absolutePathComponentsBetween orig.origSrc path; + })); + + /* + Returns the original path that is copied and returned by `"${src}"`. + This may be a subpath of a store path, for example when `src` was created + with `focusAt` or `extend`. + + Type: sourceLike -> Path + + Example: + + getOriginalFocusPath + (extend + (cleanSource ./src) + [ ./README.md ] + ) + == ./src + */ + getOriginalFocusPath = srcRaw: + let + srcAttrs = toSourceAttributes srcRaw; + in srcAttrs.origSrc + "${concatMapStrings (x: "/${x}") srcAttrs.subpath}"; + + /* + Returns the part of the path of `"${src}"` that is inside the store path + that is created for it. + This returns the empty string when `src` does not include any files or + directories outside of itself. + + Type: sourceLike -> Path + + Example: + + getSubpath + (extend + (cleanSource ./css) + [ ../common.mk ] + ) + == "resources/css" + # ^ assuming the directory containing + # this expression is named `resources`. + */ + getSubpath = srcRaw: + let + srcAttrs = toSourceAttributes srcRaw; + in + lib.concatStringsSep "/" srcAttrs.subpath; + /* + Produces a source that starts at `path` and only contains nodes that are in `src`. + + Type: + cutAt :: Path -> SourceLike -> Source + + Example: + # suppose we have files ./foo.json and ./bar/default.json + src = sources.filter (path: type: type == "directory" || hasSuffix ".json" path) ./. + "${src}/bar/default.json" + => "/nix/store/pjn...-source/bar/default.json" + # ^ exists + + "${sources.cutAt ./bar src}/default.json" + => "/nix/store/ls9...-source/default.json" + # ^ exists, hash is not sensitive to foo.json + + # contrast with focusAt + "${sources.focusAt ./bar src}/../foo.json" + => "/nix/store/pjn...-source/bar/../foo.json" + # ^ exists, hash is sensitive to both file hashes + */ + cutAt = + # The path that will form the new root of the source + path: + # A sourcelike that determines which nodes will be included, starting at `path` + srcRaw: + assert typeOf path == "path"; + let + orig = toSourceAttributes srcRaw; + valid = + if ! pathHasPrefix orig.origSrc path + then throw "sources.cutAt: new source root must be a subpath (or self) of the original source directory. But ${toString path} is not a subpath (or self) of ${toString orig.origSrc}" + else if ! pathExists path + then + throw "sources.cutAt: new source root ${toString path} does not exist." + else if ! orig.filter path (pathType path) + then + throw "sources.cutAt: new path ${toString path} is not actually included in the source. Potential causes include an incorrect path, incorrect filter function or a forgotten sources.extend call." + else x: x; + in + valid ( + fromSourceAttributes (orig // { + origSrc = path; + subpath = []; + }) + ); + + /* + Change source such that the root is at the `parent` directory, but include + nothing except source and the path leading up to it. + + Type: + sources.reparent :: Path -> SourceLike -> Source + + */ + reparent = path: source: extend (empty path) [source]; /* Add logging to a source, for troubleshooting the filtering behavior. @@ -110,9 +335,34 @@ let in builtins.trace "${attrs.name}.filter ${path} = ${boolToString r}" r; } - ) // { - satisfiesSubpathInvariant = src ? satisfiesSubpathInvariant && src.satisfiesSubpathInvariant; - }; + ); + + /* + Change the name of a source; the part after the hash in the store path. + + NOTE: `lib.sources` defaults to `source`. It is tempting to name it after the + last path component, but this was a bad default that led to unreproducable + store paths when the same directory contents were read from a different + location. + + Type: sources.setName :: String -> SourceLike -> Source + + Example: + src = with sources; setName "fizzbuzz" + (filter (path: type: !lib.hasSuffix ".nix" path) ./.); + "${src}" + => "/nix/store/cafri8rrc2bc1yvrsbmg5w6ci8rbqvzs-fizzbuzz""${src}" + + src2 = sources.setName "easypeasy" ./.; + "${src2}" + => "/nix/store/crvhqahzla2nxxyilzigwki5bkf7nfpc-easypeasy" + */ + setName = + # A string that will be the new name. It will be passed to `lib.sanitizeDerivationName` + name: + # A source-like value + src: + cleanSourceWith { name = sanitizeDerivationName name; inherit src; }; # Filter sources by a list of regular expressions. # @@ -212,7 +462,7 @@ let # Internal functions # - # toSourceAttributes : sourceLike -> SourceAttrs + # (private) toSourceAttributes : sourceLike -> SourceAttrs # # Convert any source-like object into a simple, singular representation. # We don't expose this representation in order to avoid having a fifth path- @@ -220,24 +470,48 @@ let # (Existing ones being: paths, strings, sources and x//{outPath}) # So instead of exposing internals, we build a library of combinator functions. toSourceAttributes = src: - let - isFiltered = src ? _isLibCleanSourceWith; - in - { - # The original path - origSrc = if isFiltered then src.origSrc else src; - filter = if isFiltered then src.filter else _: _: true; - name = if isFiltered then src.name else "source"; - }; + if src ? _isLibCleanSourceWith + then { + inherit (src) origSrc filter name subpath; + } + else if typeOf src == "path" then + if builtins.pathExists src + then { + origSrc = src; + filter = _: _: true; + name = "source"; + subpath = []; + } + else throw '' + Path does not exist while attempting to construct a source. + path: ${toString src}'' + else if typeOf src == "string" then + if pathHasContext src then + throw '' + Path may require a build while attempting to construct a source. + Using a build output like this is usually a bad idea because it blocks + the evaluation process for the duration of the build. It's often better + to filter built "sources" in a derivation than in an expression. + path string: ${src}'' + else + toSourceAttributes (/. + src) + else if src ? outPath && typeOf src.outPath == "path" then + # Sometimes, path-like attrsets are constructed to augment a path with + # a bit of metadata. + toSourceAttributes src.outPath + else + throw "A value of type ${typeOf src} can not be automatically converted to a source."; - # fromSourceAttributes : SourceAttrs -> Source + # (private) fromSourceAttributes : SourceAttrs -> Source # # Inverse of toSourceAttributes for Source objects. - fromSourceAttributes = { origSrc, filter, name }: - { + fromSourceAttributes = { origSrc, filter, name, subpath }: + let + root = builtins.path { inherit filter name; path = origSrc; }; + in { _isLibCleanSourceWith = true; - inherit origSrc filter name; - outPath = builtins.path { inherit filter name; path = origSrc; }; + inherit origSrc filter name subpath root; + outPath = root + "${concatMapStrings (x: "/${x}") subpath}"; }; in { @@ -257,7 +531,49 @@ in { sourceByRegex sourceFilesBySuffices + ; + # part of the interface meant to be referenced as source.* or localized + # let inherit. + inherit + # combinators + cutAt + empty + extend + focusAt + reparent + setName trace + + # getters + getOriginalFocusPath + getSubpath ; + + /* + Use a function to remove files, directories, etc from a source. + + By reducing the number of files that are available to a derivation, more + evaluations will result in the same hash and therefore do not need to be + rebuilt. + + This function is similar to `builtins.filterSource`, but more useful + because it returns a `Source` object that can be used by the other functions + in `lib.sources`. + + When nesting `sources.filter` calls, the inner call's predicate is respected + by combining it with the && operator. If you want the opposite effect, the + `sources.extend` function may be of interest. + + Type: sources.filter :: (Path -> TypeString -> Bool) -> SourceLike -> Source + + Example: + src = lib.sources.filter (path: type: !lib.hasSuffix ".nix" path) ./.; + "${src}" + => "/nix/store/cdklw7jpc3ffjxhvpzwl5aaysay07z0y-source" + src2 = lib.sources.filter (path: type: type == "directory" || lib.hasSuffix ".json" path) + "${src2}" + => "/nix/store/j7l6s884ngkdzzsnw8k3zxflsi6ax492-source" + */ + filter = _filter; } diff --git a/lib/tests/filesystem-test.nix b/lib/tests/filesystem-test.nix new file mode 100644 index 0000000000000..2866c3c84bd7e --- /dev/null +++ b/lib/tests/filesystem-test.nix @@ -0,0 +1,101 @@ +let + lib = import ../.; + inherit (lib) filesystem concatMap; + inherit (filesystem) + pathHasPrefix + pathIntersects + absolutePathComponentsBetween + commonPath + ; + inherit (import ./property-test.nix) forceChecks withItems throws; + + somePaths = [ + /. + /foo + /bar + "/bar" + /foo/foo + /foo/bar + /bar/foo/bar + /foo/bar/foo + ]; + + alternatePathHasPrefix = prefixPath: otherPath: + let + normalizedPathString = pathLike: toString (/. + pathLike); + pre = normalizedPathString prefixPath; + other = normalizedPathString otherPath; + in + if other == pre + then true + else if other == "/" + then false + else alternatePathHasPrefix pre (dirOf other); + +in + +################ +# pathHasPrefix + +# basics +assert pathHasPrefix /foo /foo; +assert pathHasPrefix /foo /foo/bar; +assert !pathHasPrefix /foo /bar; +assert pathHasPrefix /foo/a /foo/a; +assert pathHasPrefix /foo/a /foo/a/b; +assert !pathHasPrefix /foo/a/b /foo/a; +assert pathHasPrefix /. /foo; +assert !pathHasPrefix /foo /.; + +# strings are normalized +assert pathHasPrefix "/foo" "/foo/bar"; +assert pathHasPrefix "/foo" "/foo////bar"; +assert pathHasPrefix "/foo" "/foo/bar/"; +# This fits Nix's disregard for trailing slashes. Not great, but doing the opposite isn't great either. +assert pathHasPrefix "/foo/" "/foo"; +assert pathHasPrefix "/foo" "/foo/"; + +assert !pathHasPrefix /foo /.; +assert !pathHasPrefix /foo /bar/foo; +assert !pathHasPrefix /foo /foof; # not a simple string comparison + + +assert forceChecks ( + withItems "prefix" somePaths (prefix: + withItems "other" somePaths (other: + assert pathHasPrefix prefix other == alternatePathHasPrefix prefix other; {} + ) + ) +); + + +######################## +# absolutePathComponentsBetween + +assert absolutePathComponentsBetween /a /a/b/c == ["b" "c"]; +assert absolutePathComponentsBetween /a/b /a/b/c == ["c"]; +assert absolutePathComponentsBetween /a/b/c /a/b/c == []; +assert throws (absolutePathComponentsBetween /a /.); +assert throws (absolutePathComponentsBetween /a /b); + + +################ +# commonPath + +assert commonPath /a /a == /a; +assert commonPath /a "/a" == /a; +assert commonPath "/a" /a == /a; + +assert commonPath /a /a/b == /a; +assert commonPath /a/b /a == /a; + +assert commonPath /a /a/b == /a; +assert commonPath /a/b /a/c == /a; +assert commonPath /a/b /a/c/d == /a; +assert commonPath /a/b/c /a/b/d == /a/b; +assert commonPath /a/b/c /a/b/c == /a/b/c; +assert commonPath /a/b /. == /.; +assert commonPath /. /. == /.; + +{} + diff --git a/lib/tests/filesystem.sh b/lib/tests/filesystem.sh new file mode 100755 index 0000000000000..a809d36423e64 --- /dev/null +++ b/lib/tests/filesystem.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Use +# || die +die() { + echo >&2 "test case failed: " "$@" + exit 1 +} + +cd "$(dirname ${BASH_SOURCE[0]})" + +nix-instantiate --eval --json --show-trace filesystem-test.nix >/dev/null || die filesystem-test.nix + +echo tests ok diff --git a/lib/tests/property-test.nix b/lib/tests/property-test.nix new file mode 100644 index 0000000000000..c564b0d5240e0 --- /dev/null +++ b/lib/tests/property-test.nix @@ -0,0 +1,86 @@ + +/* +This "property testing framework" leverages built-in asserts and traces in +an attempt to provide the best feedback it can give with the limitations +that: + +- the test writing experience is weird +- only the first error is reported +- no shrinking +- no fuzzing + +To use it, stick to this pattern: + + assert forceChecks ( + withItems "prefix" somePaths (prefix: + withItems "other" somePaths (other: + assert pathHasPrefix prefix other == alternatePathHasPrefix prefix other; {} + ) + ) + ); + +Let's have a look why everything is there. + + assert pathHasPrefix prefix other == alternatePathHasPrefix prefix other; {} + ^^^^^^ + reports the line number of the test failure, with an augmented stack + trace that contains the values that triggered the failure + + + assert pathHasPrefix prefix other == alternatePathHasPrefix prefix other; {} + ^^ + return non-bool, so we can detect when someone forgets + the essential assert keyword + + + withItems "other" somePaths (other: + ^^^^^^^^^ + lets us do limited property testing, using examples instead of random generations + + + withItems "other" somePaths (other: + ^^^^^^^ + names the example when it is printed in the stack trace, so you know for + which values it failed + + + withItems "other" somePaths (other: + ^^^^^^^^^ ^^^^^ + a list of examples to test with. `other` will have each of these values + + nesting of withItems: + tests all possible combinations + + assert + ^^^^^^ + while not essential, it's nice to reuse a syntax that doesn't produce + O(n) indentation pyramids. Lists would qualify for this purpose but turned + out to be messier. + + assert forceChecks ( + ^^^^^^^^^^^ + because we want to detect accidental omissions of `assert`, we can't + normally return true, `forceChecks` forces its argument and then returns true. + + +*/ + +let + lib = import ../.; + inherit (builtins) concatMap seq addErrorContext; + inherit (lib.generators) toPretty; + + forceChecks = x: builtins.seq x true; + checkAssert = r: if r == true || r == false then throw "Please use assert expressions in forceChecks and withItems." else r; + withItems = name: items: f: + lib.foldl' + (_prev: item: addErrorContext "while testing with ${name} = ${toPretty {} item}" (checkAssert (f item))) + true + items; + + # true iff the argument throws an exception + throws = a: ! (builtins.tryEval a).success; + +in { + inherit forceChecks withItems throws; +} diff --git a/lib/tests/release.nix b/lib/tests/release.nix index c3b05251f709d..424a800c9706a 100644 --- a/lib/tests/release.nix +++ b/lib/tests/release.nix @@ -32,5 +32,8 @@ pkgs.runCommandNoCC "nixpkgs-lib-tests" { echo "Running lib/tests/sources.sh" TEST_LIB=$PWD/lib bash lib/tests/sources.sh + echo "Running lib/tests/filesystem.sh" + bash lib/tests/filesystem.sh + touch $out '' diff --git a/lib/tests/sources.sh b/lib/tests/sources.sh index 71fee719cb21e..9e1f3c060b3d4 100755 --- a/lib/tests/sources.sh +++ b/lib/tests/sources.sh @@ -47,6 +47,27 @@ dir="$(nix eval --raw '(with import ; "${ EOF ) || die "cleanSourceWith 1" + +dir="$(nix eval --raw '(with import ; "${ + sources.filter (path: type: ! hasSuffix ".bar" path) ./. +}")')" +(cd $dir; find) | sort -f | diff -U10 - <(cat <; with sources; "${ + focusAt ./src ( + extend + ./README.md + [ ./foo.bar ] + ) + }")' || true + ) 2>&1 | grep 'is not actually included in the source. Potential causes include an incorrect path, incorrect filter function or a forgotten sources.extend call.' >/dev/null +) || die "sources.focusAt does not cause accidental inclusion" + +dir="$( + nix eval --raw '(with import ; with sources; "${ + cutAt ./src ( + extend + ./README.md + [ (cleanSource ./src) ] + ) + }")' +)" +(cd "$dir" && find .) | sort -f | diff -U10 - <(cat </dev/null) || die "sources.cutAt produces a plain store path; not a subpath of a store path" + +dir="$( + nix eval --raw '(with import ; with sources; "${ + extend + (cleanSource ./src) + [ ./README.md + ./foo.bar + ] + }")' +)" +(cd "$dir" && find ..) | sort -f | diff -U10 - <(cat </dev/null || die "setName" + +{ + ( + nix eval --raw '(with import ; with sources; "${ + extend ./. [ ./does-not-exist ] + }")' || true + ) 2>&1 | grep 'error: Path does not exist while attempting to construct a source.' >/dev/null +} || die "non-existing paths are an error, even when used lazily in, say, extend's list argument." +echo $dir | grep -- -anna >/dev/null || die "setName" + echo >&2 tests ok diff --git a/pkgs/stdenv/generic/make-derivation.nix b/pkgs/stdenv/generic/make-derivation.nix index 4536024c51183..fd18403c83b46 100644 --- a/pkgs/stdenv/generic/make-derivation.nix +++ b/pkgs/stdenv/generic/make-derivation.nix @@ -89,6 +89,8 @@ in rec { , patches ? [] + , src ? null + , __contentAddressed ? (! attrs ? outputHash) # Fixed-output drvs can't be content addressed too && (config.contentAddressedByDefault or false) @@ -264,6 +266,26 @@ in rec { inherit doCheck doInstallCheck; inherit outputs; + + src = + if attrs ? src.root && attrs ? src.subpath then + src.root + else + src; + + subpath = + if attrs ? src.root && attrs ? src.subpath && attrs.src.subpath != [] then + lib.concatStringsSep "/" src.subpath + else + null; + + # TODO simplify by adding to unpackPhase instead. + # I haven't done it yet to avoid a mass rebuild while working on this. + postUnpack = if attrs ? src.root && attrs ? src.subpath && attrs.src.subpath != [] then '' + sourceRoot="$sourceRoot/$subpath" + ${attrs.postUnpack or ""} + '' else attrs.postUnpack or null; + } // lib.optionalAttrs (__contentAddressed) { inherit __contentAddressed; # Provide default values for outputHashMode and outputHashAlgo because