Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow nixpkgs-check-by-name to work with recent Nix versions #79

Merged
merged 3 commits into from
Jul 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,5 @@ relative-path = "1.9.3"
textwrap = "0.16.1"

[dev-dependencies]
pretty_assertions = "1.4.0"
temp-env = "0.3.6"
29 changes: 14 additions & 15 deletions default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -161,21 +161,20 @@ let
'';
};

# Tests the tool on the pinned Nixpkgs tree, this is a good sanity check
nixpkgsCheck =
pkgs.runCommand "test-nixpkgs-check-by-name"
{
nativeBuildInputs = [
packages.build
pkgs.nix
];
nixpkgsPath = nixpkgs;
}
''
${initNix}
nixpkgs-check-by-name --base "$nixpkgsPath" "$nixpkgsPath"
touch $out
'';
# Tests the tool on the pinned Nixpkgs tree with stable Nix. This is a good sanity check.
nixpkgsCheck = pkgs.callPackage ./nixpkgs-check.nix {
inherit initNix nixpkgs;
nixpkgs-check-by-name = packages.build;
nix = pkgs.nixVersions.stable;
};

# Tests the tool on the pinned Nixpkgs tree with various Nix and Lix versions.
# This allows exposure to changes in behavior from Nix and Nix-alikes.
nixpkgsCheckWithLatestNix = packages.nixpkgsCheck.nixVersions.latest;
nixpkgsCheckWithGitNix = packages.nixpkgsCheck.nixVersions.git;
nixpkgsCheckWithMinimumNix = packages.nixpkgsCheck.nixVersions.minimum;
nixpkgsCheckWithStableLix = packages.nixpkgsCheck.lixVersions.stable;
nixpkgsCheckWithLatestLix = packages.nixpkgsCheck.lixVersions.latest;
Comment on lines +171 to +177
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is slowing down CI by about 3.5 minutes. I opened #83 instead to allow running the test suite with different Nix versions, which this PR can make use of.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is way less important now that #83 is merged and the tool doesn't depend on the ambient version of Nix. I'm fine cutting them all, or going down to {minimum,stable,latest}.

That said, given that reviews are usually a matter of a week or more, 3.5 minutes more isn't an actual issue. Amdahl's law!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I decided to keep these in the CI run. The number of PRs that aren't you or I is quite low.

};
in
packages
Expand Down
56 changes: 56 additions & 0 deletions nixpkgs-check.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
{
lib,
runCommand,
nixpkgs-check-by-name,
initNix,
nixpkgs,
nix,
nixVersions,
lixVersions,
}:
let
# Given an attrset, return only the values that both eval and are derivations.
#
# We do this to avoid encoding information about which names are in present in the attrset.
# For instance, `nixVersions` contains `nix_2_10`, which throws, and `lixVersions` does not
# contain `minimum` or `git`, but `nixVersions` does.
#
# Let's just map over those attrsets and return what's useful from there.
derivationsFromAttrset =
attrset:
lib.filterAttrs (
name: value:
let
eval = builtins.tryEval value;
in
eval.success && lib.isDerivation eval.value
) attrset;

mkNixpkgsCheck =
name: nix:
runCommand "test-nixpkgs-check-by-name-with-${nix.name}"
{
nativeBuildInputs = [
nixpkgs-check-by-name
nix
];

env.NIX_CHECK_BY_NAME_NIX_PACKAGE = lib.getBin nix;

passthru = {
# Allow running against all other Nix versions.
nixVersions = lib.mapAttrs mkNixpkgsCheck (derivationsFromAttrset nixVersions);

# Allow running against all other Lix versions.
lixVersions = lib.mapAttrs mkNixpkgsCheck (derivationsFromAttrset lixVersions);
};
}
''
${initNix}
# This is what nixpkgs-check-by-name uses
export NIX_CHECK_BY_NAME_NIX_PACKAGE=${lib.getBin nix}
${nixpkgs-check-by-name}/bin/.nixpkgs-check-by-name-wrapped --base "${nixpkgs}" "${nixpkgs}"
touch $out
'';
in
mkNixpkgsCheck nix.name nix
4 changes: 4 additions & 0 deletions package.nix
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,17 @@
rustPlatform,
nix,
nixVersions,
lixVersions,
clippy,
makeWrapper,

nixVersionsToTest ? [
nix
nixVersions.stable
nixVersions.minimum
nixVersions.latest
lixVersions.stable
lixVersions.latest
],

nixpkgsLibPath,
Expand Down
148 changes: 111 additions & 37 deletions src/nix_file.rs
Original file line number Diff line number Diff line change
Expand Up @@ -187,12 +187,12 @@ impl NixFile {
};

if ast::Attr::can_cast(node.kind()) {
// Something like `foo`, `"foo"` or `${"foo"}`
// Something like `foo`, `"foo"` or `${"foo"}`.
// For Nix versions >= 2.21, this is also `foo` in `inherit (bar) foo`.
} else if ast::Inherit::can_cast(node.kind()) {
// Something like `inherit <attr>` or `inherit (<source>) <attr>`.
// This is the only other way how `builtins.unsafeGetAttrPos` can return
// attribute positions, but we only look for ones like `<attr-path> = <value>`,
// so ignore this.
// For Nix versions <= 2.20, this is something like `inherit <attr>` or
// `inherit (<source>) <attr>`. Since we only look for expressions like
// `<attr-path> = <value>`, ignore this.
return Ok(Left(node.to_string()));
} else {
// However, anything else is not expected and smells like a bug.
Expand All @@ -210,6 +210,13 @@ impl NixFile {
)
};

// For Nix versions >= 2.21, an `ast::Attr` can have a parent of `ast::Inherit`.
if ast::Inherit::can_cast(attrpath_node.kind()) {
// Something like `inherit <attr>` or `inherit (<source>) <attr>`.
// We only look for ones like `<attr-path> = <value>`, so ignore this.
return Ok(Left(node.to_string()));
};

philiptaron marked this conversation as resolved.
Show resolved Hide resolved
if !ast::Attrpath::can_cast(attrpath_node.kind()) {
// We know that `node` is an attribute, so its parent should be an attribute path.
anyhow::bail!(
Expand Down Expand Up @@ -449,20 +456,28 @@ mod tests {
use super::*;
use crate::tests;
use indoc::indoc;
use pretty_assertions::assert_eq;
use std::collections::BTreeMap;

#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
struct Position {
line: usize,
column: usize,
}

#[test]
fn detects_attributes() -> anyhow::Result<()> {
let temp_dir = tests::tempdir()?;
let file = temp_dir.path().join("file.nix");
let contents = indoc! {r#"
toInherit: {
foo = 1;
"bar" = 2;
${"baz"} = 3;
"${"qux"}" = 4;
testG: {
testA = 1;
"testB" = 2;
${"testC"} = 3;
"${"testD"}" = 4;

# A
quux
testE
# B
=
# C
Expand All @@ -471,42 +486,101 @@ mod tests {
;
# E

/**/quuux/**/=/**/5/**/;/*E*/
/**/testF/**/=/**/5/**/;/*E*/

inherit toInherit;
inherit (toInherit) toInherit;
inherit testG;
inherit ({}) testH;

inherit ({})
testI testJ
testK testL;
}
"#};

std::fs::write(&file, contents)?;

let nix_file = NixFile::new(&file)?;

// These are `builtins.unsafeGetAttrPos` locations for the attributes
// These are `builtins.unsafeGetAttrPos` locations for the attributes, generated by saving
// the Nix file above as `test.nix`, then making this `driver.nix` file.
//
// ```nix
// let
// value = import ./test.nix null;
// info =
// name:
// let
// inherit (builtins.unsafeGetAttrPos name value) line column;
// in
// _: ''(${toString line}, ${toString column}, Left("fill me in")),'';
// in
// builtins.mapAttrs info value
// ```
//
// Then, run this command to get the raw test data.
//
// ```
// for v in Versions.{stable,minimum,latest} lixVersions.{stable,latest}; do
// nix-instantiate --strict --eval --json driver.nix | jq -r .[]
// done | sort -u
// ```
//
// Fill in the expected data until the test passes.
let cases = [
(2, 3, Right("foo = 1;")),
(3, 3, Right(r#""bar" = 2;"#)),
(4, 3, Right(r#"${"baz"} = 3;"#)),
(5, 3, Right(r#""${"qux"}" = 4;"#)),
(8, 3, Right("quux\n # B\n =\n # C\n 5\n # D\n ;")),
(17, 7, Right("quuux/**/=/**/5/**/;")),
(19, 10, Left("inherit toInherit;")),
(20, 22, Left("inherit (toInherit) toInherit;")),
// generated with all Nix versions
(2, 3, Right("testA = 1;")),
// generated with all Nix versions
(3, 3, Right("\"testB\" = 2;")),
// generated with all Nix versions
(4, 3, Right("${\"testC\"} = 3;")),
// generated with all Nix versions
(5, 3, Right("\"${\"testD\"}\" = 4;")),
// generated with all Nix versions
(8, 3, Right("testE\n # B\n =\n # C\n 5\n # D\n ;")),
// generated on all Nix versions
(17, 7, Right("testF/**/=/**/5/**/;")),
// generated with Nix <= 2.20
(19, 10, Left("inherit testG;")),
// generated with Nix >= 2.21
(19, 11, Left("testG")),
// generated with Nix <= 2.20
(20, 15, Left("inherit ({}) testH;")),
// generated with Nix >= 2.21
(20, 16, Left("testH")),
// generated with Nix <= 2.20
(
22,
15,
Left("inherit ({})\n testI testJ\n testK testL;"),
),
// generated with Nix >= 2.21
(23, 5, Left("testI")),
// generated with Nix >= 2.21
(23, 11, Left("testJ")),
// generated with Nix >= 2.21
(24, 5, Left("testK")),
// generated with Nix >= 2.21
(24, 11, Left("testL")),
philiptaron marked this conversation as resolved.
Show resolved Hide resolved
];

for (line, column, expected_result) in cases {
let actual_result = nix_file
.attrpath_value_at(line, column)
.context(format!("line {line}, column {column}"))?
.map_right(|node| node.to_string());
let owned_expected_result = expected_result
.map(|x| x.to_string())
.map_left(|x| x.to_string());
assert_eq!(
actual_result, owned_expected_result,
"line {line}, column {column}"
);
}
let expected = BTreeMap::from_iter(cases.map(|(line, column, result)| {
(
Position { line, column },
result
.map(|node| node.to_string())
.map_right(|node| node.to_string()),
)
}));
let actual = BTreeMap::from_iter(cases.map(|(line, column, _)| {
(
Position { line, column },
nix_file
.attrpath_value_at(line, column)
.context(format!("line {line}, column {column}"))
.expect("value")
.map_right(|node| node.to_string()),
)
}));
assert_eq!(expected, actual);

Ok(())
}
Expand Down