mirror of
https://github.com/ipetkov/crane.git
synced 2024-11-23 08:22:41 +03:00
Merge pull request #32 from ipetkov/fix-dummy-invalidation
mkDummySrc: fix cache invalidation if src is flake root
This commit is contained in:
commit
cec8c8674c
2
.github/workflows/test.yml
vendored
2
.github/workflows/test.yml
vendored
@ -32,6 +32,8 @@ jobs:
|
||||
authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
|
||||
- name: flake checks
|
||||
run: nix flake check --print-build-logs ${{ matrix.nixpkgs-override }}
|
||||
- name: extra tests
|
||||
run: nix develop ${{ matrix.nixpkgs-override }} --command ./extra-tests/test.sh
|
||||
- name: validate examples
|
||||
run: |
|
||||
for f in $(find examples -maxdepth 1 -mindepth 1 -type d); do
|
||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,3 +1,4 @@
|
||||
/examples/*/flake.lock
|
||||
/result*
|
||||
target/
|
||||
/extra-tests/*/flake.lock
|
||||
|
@ -6,6 +6,12 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
|
||||
|
||||
## Unreleased
|
||||
|
||||
### Fixed
|
||||
* Dummy source derivations go to greater lengths to only depend on the files
|
||||
they consume. Specifying the entire flake source as an input (e.g. via
|
||||
`buildPackage { src = self; }`) now avoids rebuilding everything from scratch
|
||||
whenever _any_ file is changed. #28
|
||||
|
||||
## [0.4.0] - 2022-05-10
|
||||
|
||||
### Changed
|
||||
|
16
extra-tests/dummy-does-not-depend-on-flake-source-via-path/Cargo.lock
generated
Normal file
16
extra-tests/dummy-does-not-depend-on-flake-source-via-path/Cargo.lock
generated
Normal file
@ -0,0 +1,16 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 3
|
||||
|
||||
[[package]]
|
||||
name = "byteorder"
|
||||
version = "1.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610"
|
||||
|
||||
[[package]]
|
||||
name = "simple"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
]
|
@ -0,0 +1,7 @@
|
||||
[package]
|
||||
name = "simple"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
byteorder = "*"
|
@ -0,0 +1,14 @@
|
||||
{
|
||||
inputs = {
|
||||
crane.url = "github:ipetkov/crane";
|
||||
nixpkgs.follows = "crane/nixpkgs";
|
||||
flake-utils.follows = "crane/flake-utils";
|
||||
};
|
||||
|
||||
outputs = { self, nixpkgs, flake-utils, crane }: flake-utils.lib.eachDefaultSystem
|
||||
(system: {
|
||||
packages.dummy = crane.lib.${system}.mkDummySrc {
|
||||
src = ./.;
|
||||
};
|
||||
});
|
||||
}
|
22
extra-tests/dummy-does-not-depend-on-flake-source-via-path/test.sh
Executable file
22
extra-tests/dummy-does-not-depend-on-flake-source-via-path/test.sh
Executable file
@ -0,0 +1,22 @@
|
||||
#!/bin/sh
|
||||
|
||||
set -eu
|
||||
|
||||
scriptDir=$(dirname "$0")
|
||||
cd "${scriptDir}"
|
||||
|
||||
craneOverride="--override-input crane ../.."
|
||||
flakeSrc=$(nix flake metadata ${craneOverride} --json 2>/dev/null | jq -r '.path')
|
||||
|
||||
# Get information about the default derivation
|
||||
# Then pull out any input sources
|
||||
drvSrcs=$(nix show-derivation ${craneOverride} '.#dummy' 2>/dev/null |
|
||||
jq -r 'to_entries[].value.inputSrcs[]')
|
||||
|
||||
# And lastly make sure we DO NOT find the flake root source listed
|
||||
# or else the dummy derivation will depend on _too much_ (and get
|
||||
# invalidated with irrelevant changes)
|
||||
if echo "${drvSrcs}" | grep -q -F "${flakeSrc}"; then
|
||||
echo "error: dummy derivation depends on flake source"
|
||||
exit 1
|
||||
fi
|
16
extra-tests/dummy-does-not-depend-on-flake-source-via-self/Cargo.lock
generated
Normal file
16
extra-tests/dummy-does-not-depend-on-flake-source-via-self/Cargo.lock
generated
Normal file
@ -0,0 +1,16 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 3
|
||||
|
||||
[[package]]
|
||||
name = "byteorder"
|
||||
version = "1.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610"
|
||||
|
||||
[[package]]
|
||||
name = "simple"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
]
|
@ -0,0 +1,7 @@
|
||||
[package]
|
||||
name = "simple"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
byteorder = "*"
|
@ -0,0 +1,14 @@
|
||||
{
|
||||
inputs = {
|
||||
crane.url = "github:ipetkov/crane";
|
||||
nixpkgs.follows = "crane/nixpkgs";
|
||||
flake-utils.follows = "crane/flake-utils";
|
||||
};
|
||||
|
||||
outputs = { self, nixpkgs, flake-utils, crane }: flake-utils.lib.eachDefaultSystem
|
||||
(system: {
|
||||
packages.dummy = crane.lib.${system}.mkDummySrc {
|
||||
src = self;
|
||||
};
|
||||
});
|
||||
}
|
22
extra-tests/dummy-does-not-depend-on-flake-source-via-self/test.sh
Executable file
22
extra-tests/dummy-does-not-depend-on-flake-source-via-self/test.sh
Executable file
@ -0,0 +1,22 @@
|
||||
#!/bin/sh
|
||||
|
||||
set -eu
|
||||
|
||||
scriptDir=$(dirname "$0")
|
||||
cd "${scriptDir}"
|
||||
|
||||
craneOverride="--override-input crane ../.."
|
||||
flakeSrc=$(nix flake metadata ${craneOverride} --json 2>/dev/null | jq -r '.path')
|
||||
|
||||
# Get information about the default derivation
|
||||
# Then pull out any input sources
|
||||
drvSrcs=$(nix show-derivation ${craneOverride} '.#dummy' 2>/dev/null |
|
||||
jq -r 'to_entries[].value.inputSrcs[]')
|
||||
|
||||
# And lastly make sure we DO NOT find the flake root source listed
|
||||
# or else the dummy derivation will depend on _too much_ (and get
|
||||
# invalidated with irrelevant changes)
|
||||
if echo "${drvSrcs}" | grep -q -F "${flakeSrc}"; then
|
||||
echo "error: dummy derivation depends on flake source"
|
||||
exit 1
|
||||
fi
|
24
extra-tests/test.sh
Executable file
24
extra-tests/test.sh
Executable file
@ -0,0 +1,24 @@
|
||||
#!/bin/sh
|
||||
|
||||
set -eu
|
||||
|
||||
anyFailed=0
|
||||
|
||||
runTest() {
|
||||
local testPath="$1"
|
||||
|
||||
if ${testPath}; then
|
||||
echo "pass: ${testPath}"
|
||||
else
|
||||
echo "fail: ${testPath}"
|
||||
anyFailed=1
|
||||
fi
|
||||
}
|
||||
|
||||
scriptPath=$(dirname "$0")
|
||||
cd "${scriptPath}"
|
||||
|
||||
runTest ./dummy-does-not-depend-on-flake-source-via-path/test.sh
|
||||
runTest ./dummy-does-not-depend-on-flake-source-via-self/test.sh
|
||||
|
||||
exit ${anyFailed}
|
@ -81,6 +81,7 @@
|
||||
|
||||
devShell = pkgs.mkShell {
|
||||
nativeBuildInputs = with pkgs; [
|
||||
jq
|
||||
nixpkgs-fmt
|
||||
];
|
||||
};
|
||||
|
@ -7,7 +7,7 @@
|
||||
}:
|
||||
|
||||
{ src
|
||||
, cargoLock ? src + "/Cargo.lock"
|
||||
, cargoLock ? null
|
||||
, ...
|
||||
}:
|
||||
let
|
||||
@ -23,6 +23,93 @@ let
|
||||
|
||||
inherit (lib.strings) concatStrings;
|
||||
|
||||
# A quick explanation of what is happening here and why it is done in the way
|
||||
# that it is:
|
||||
#
|
||||
# We want to build a dummy version of the project source. The only things that
|
||||
# we want to keep are:
|
||||
# 1. the Cargo.lock file
|
||||
# 2. any .cargo/config.toml files (unaltered)
|
||||
# 3. any Cargo.toml files stripped down only the attributes that would affect
|
||||
# caching dependencies
|
||||
#
|
||||
# Any other sources are completely ignored, and so, we want to avoid any of those ignored sources
|
||||
# leading to invalidating our caches. Normally if a build script references any data from another
|
||||
# derivation, Nix will consider that entire derivation as an input and any changes to it or its
|
||||
# inputs would invalidate the consumer. But we can try to get a bit clever:
|
||||
#
|
||||
# If we "break up" the input source into smaller parts (i.e. only the parts we care about) we can
|
||||
# avoid the "false dependency" invalidation. One trick to accomplishing this is by "laundering"
|
||||
# the data at evaluation time: we have Nix read the data out, do some TOML transformations, write
|
||||
# it to a fresh file, and then have other derivations consume _that result_. The only thing we
|
||||
# have to be careful about is stripping away any "context" Nix may be tracking for the input as we
|
||||
# work with it: specifically if the `src` input provided by the caller happens to point to a
|
||||
# derivation output (e.g. including using an entire flake source like `{ src = self; }`). Nix
|
||||
# carries this "context" to any other strings operations which touch the derivation output (e.g.
|
||||
# appending path components). In most cases this is the right thing do to since a derivation which
|
||||
# consumes any part of another derivation _probably_ needs to be rebuilt if the latter changes,
|
||||
# but here we will explicitly strip the context out since we want to break this dependency.
|
||||
# This way, adding a comment or editing an ignored field won't lead to rebuilding everything from
|
||||
# scratch!
|
||||
#
|
||||
# The other trick to accomplishing a similar feat (but without rewriting the files at evaluation
|
||||
# time) is to use Nix's source filtering. We give Nix some source path and a function, and Nix
|
||||
# will create a brand new entry in the store after while asking the function whether each and
|
||||
# every file or directory under said path should be kept or not. The result is that only changes
|
||||
# to the kept files would result in rebuilding the consumers.
|
||||
#
|
||||
# Thus to avoid accidental rebuilds, we need to explicitly filter the source to only contain the
|
||||
# files we care about (namely, the .cargo/config.toml files and the Cargo.lock file, we're already
|
||||
# cleaning up the Cargo.toml files during evaluation). There is one extra hurdle we have to clear:
|
||||
# Nix's source filtering operates in a top-down lazy manner. For every directory it encounters it
|
||||
# will ask "should this be kept or not?" If the answer is "no" it skips the directory entirely,
|
||||
# which is reasonable. The problem is the function won't know what files that directory may or may
|
||||
# not contain unless it indicates the directory should be kept. If that happens but the function
|
||||
# rejects all other files under the directory, Nix just keeps the (now empty) directory and moves
|
||||
# on. This isn't a huge problem, except that adding a new directory _anywhere_ in the flake root
|
||||
# would also invalidate everything again.
|
||||
#
|
||||
# Finally we pull one last trick up our sleeve: we do the filtering in two passes! First, at
|
||||
# evaluation time, we walk the input path and look for any interesting files (i.e.
|
||||
# .cargo/config.toml and Cargo.lock files) and remember at what paths they appear relative to the
|
||||
# input source. Then we run the source filtering and use that information to guide which files are
|
||||
# kept. Namely, if the path being filtered is a regular file, we check if its path (relative to
|
||||
# the source root) matches one of our interesting files. If the path being filtered is a
|
||||
# directory, we check if it happens to be an ancestor for an interesting file (i.e. is a prefix of
|
||||
# an interesting file). That way we are left with the smallest possible source needed for our
|
||||
# dummy derivation, and we bring any cache invalidation to a minimum. Whew!
|
||||
mkBasePath = p: (toString p) + "/";
|
||||
uncleanSrcBasePath = mkBasePath src;
|
||||
|
||||
uncleanFiles = findCargoFiles src;
|
||||
|
||||
cargoTomlsBase = uncleanSrcBasePath;
|
||||
inherit (uncleanFiles) cargoTomls;
|
||||
|
||||
cleanSrc =
|
||||
let
|
||||
adjustPaths = builtins.map (p: removePrefix uncleanSrcBasePath (toString p));
|
||||
allUncleanFiles = map
|
||||
(p: removePrefix uncleanSrcBasePath (toString p))
|
||||
# Allow the default `Cargo.lock` location to be picked up here
|
||||
# (if it exists) so it automattically appears in the cleaned source
|
||||
(uncleanFiles.cargoConfigs ++ [ "Cargo.lock" ]);
|
||||
in
|
||||
lib.cleanSourceWith {
|
||||
inherit src;
|
||||
name = "cleaned-mkDummySrc";
|
||||
filter = path: type:
|
||||
let
|
||||
strippedPath = removePrefix uncleanSrcBasePath path;
|
||||
filter = x:
|
||||
if type == "directory" then
|
||||
lib.hasPrefix strippedPath x
|
||||
else
|
||||
x == strippedPath;
|
||||
in
|
||||
lib.any filter allUncleanFiles;
|
||||
};
|
||||
|
||||
dummyrs = writeText "dummy.rs" ''
|
||||
#![allow(dead_code)]
|
||||
pub fn main() {}
|
||||
@ -33,28 +120,14 @@ let
|
||||
cp -f ${dummyrs} ${prefix}/${path}
|
||||
'';
|
||||
|
||||
inherit (findCargoFiles src)
|
||||
cargoTomls
|
||||
cargoConfigs;
|
||||
|
||||
basePath = (toString src) + "/";
|
||||
copyAllCargoConfigs = concatStrings (map
|
||||
(p:
|
||||
let
|
||||
dest = removePrefix basePath (toString p);
|
||||
in
|
||||
''
|
||||
mkdir -p $out/${dirOf dest}
|
||||
cp ${p} $out/${dest}
|
||||
''
|
||||
)
|
||||
cargoConfigs
|
||||
);
|
||||
|
||||
copyAndStubCargoTomls = concatStrings (map
|
||||
(p:
|
||||
let
|
||||
cargoTomlDest = removePrefix basePath (toString p);
|
||||
# Safety: all the paths here are fully processed/consumed at evaluation time, so it is is
|
||||
# safe to throw away any context (to the Nix store) the original path may have carried.
|
||||
# Given that we call `cleanSourceWith` earlier, we know that the input `src` must be valid
|
||||
# (or else we would have other errors to deal with)
|
||||
cargoTomlDest = builtins.unsafeDiscardStringContext (removePrefix cargoTomlsBase (toString p));
|
||||
parentDir = "$out/${dirOf cargoTomlDest}";
|
||||
|
||||
trimmedCargoToml = cleanCargoToml {
|
||||
@ -94,14 +167,19 @@ let
|
||||
cargoTomls
|
||||
);
|
||||
|
||||
# Since we allow the caller to provide a path to *some* Cargo.lock file
|
||||
# we include it in our dummy build only if it was explicitly specified
|
||||
# and the path exists.
|
||||
copyCargoLock =
|
||||
if pathExists cargoLock
|
||||
if cargoLock == null
|
||||
then ""
|
||||
else if pathExists cargoLock
|
||||
then "cp ${cargoLock} $out/Cargo.lock"
|
||||
else "echo could not find Cargo.lock at src root";
|
||||
else throw "`cargoLock` was specified but the path it points to does not exist: ${cargoLock}";
|
||||
in
|
||||
runCommandLocal "dummy-src" { } ''
|
||||
mkdir -p $out
|
||||
cp --recursive --no-preserve=mode,ownership ${cleanSrc}/. -t $out
|
||||
${copyCargoLock}
|
||||
${copyAllCargoConfigs}
|
||||
${copyAndStubCargoTomls}
|
||||
''
|
||||
|
Loading…
Reference in New Issue
Block a user