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

tests.nixpkgs-check-by-name: Implement gradual empty arg check migration #272395

Merged
merged 10 commits into from
Dec 15, 2023

Conversation

infinisil
Copy link
Member

@infinisil infinisil commented Dec 6, 2023

Context

This is part of the plan to migrate all packages to pkgs/by-name as detailed in #258650.

In #256792 I implemented support for the nixpkgs-check-by-name tool to complain when there's an unnecessary definition in all-packages.nix with an empty second argument like

foo = ../by-name/fo/foo/package.nix { };

However this was never enabled in CI with --version v1.
While at least in master there's no package where this applies, this is a perfect test case to make sure the tool can increase strictness over time without causing master breakages, as further detailed in #256788.

After thinking about it for too long, I realised that this is actually pretty easy to do after the changes in #261939 and #257735.

Changes

  • The --version flag is removed again, it was never used in CI, and it's not part of the public interface, so this is fine
  • The --base <NIXPKGS> flag is introduced, allowing the tool to check against granular regressions. For the empty argument check this means that existing empty arguments can stay, but new ones can't be introduced.

Example

If the base Nixpkgs has

# all-packages.nix
{
  foo = callPackage ../by-name/fo/foo/package.nix {
    withGui = true;
  };
}

And the main Nixpkgs has

# all-packages.nix
{
  foo = callPackage ../by-name/fo/foo/package.nix { };
}

Then the check fails because of the empty argument check.

But if you go the other way, the check succeeds!

Implications

While this isn't very useful for the empty argument check specifically, the same approach can be used to also ensure that all new packages using callPackage use pkgs/by-name.

This means that we can have incremental (and automated!) PRs to do the migration, without worrying about the base branch always introducing new violations!

Things done

  • Tidy code
  • Updated documentation
  • Added tests

Follow-up PR

Once the new tooling version propagates to the nixos-unstable channel, update CI to pass the --base flag.


Add a 👍 reaction to pull requests you find important.

@blaggacao
Copy link
Contributor

Nice solution!

…problems

This makes it such that these two errors can both be thrown for a single
package:
- The attribute value not being a derivation
- The attribute not being a proper callPackage

The tests had to be adjusted to only throw the error they were testing
for
Convenience function to run another validation over a successful validation result.

This will be usable in more locations in future commits, making the code
nicer.
This prepares the code base for the removal of the `--version` flag, to
be replaced with a flag that can specify a base version to compare the
main Nixpkgs against, in order to have gradual transitions to stricter
checks.

This refactoring does:
- Introduce the `version` module that can house the logic to increase
  strictness, with a `version::Nixpkgs` struct that contains the
  strictness conformity of a single Nixpkgs version
- Make the check return `version::Nixpkgs`
- Handle the behavior of the still-existing `--version` flag with `version::Nixpkgs`
- Introduce an intermediate `process` function to handle the top-level
  logic, especially useful in the next commit
This implements the option for a gradual migration to stricter checks.
For now this is only done for the check against empty non-auto-called
callPackage arguments, but in the future this can be used to ensure all
new packages make use of `pkgs/by-name`.

This is implemented by adding a `--base <BASE_NIXPKGS>` flag, which then
compares the base nixpkgs against the main nixpkgs version, making sure
that there are no regressions.

The `--version` flag is removed. While it was implemented, it was never
used in CI, so this is fine.
This implements the ability to test gradual transitions in check
strictness, and adds one such test for the empty non-auto-called
arguments check.
@infinisil infinisil marked this pull request as ready for review December 14, 2023 03:08
Copy link
Contributor

@philiptaron philiptaron left a comment

Choose a reason for hiding this comment

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

I did end up taking a look through the PR. It all looks sane. I have various code organization suggestions, but the current organization isn't bad at all and I would definitely accept it.

I haven't had a chance to run the code or tests myself, which is the only reason I'm not signing off as-is.

pkgs/test/nixpkgs-check-by-name/src/eval.rs Outdated Show resolved Hide resolved
- Arguments:
- `<BASE_NIXPKGS>`: The path to the Nixpkgs to check against
Copy link
Contributor

Choose a reason for hiding this comment

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

If not specified, ...

Copy link
Member Author

Choose a reason for hiding this comment

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

Good point! Also had to fix the behavior in that case (though it doesn't matter much): 53b43ce

nixpkgs_path: &Path,
package_names: Vec<String>,
eval_accessible_paths: Vec<&Path>,
) -> validation::Result<()> {
eval_accessible_paths: &Vec<&Path>,
Copy link
Contributor

Choose a reason for hiding this comment

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

One "rusty" nit is to have the various things accepting &Vec<Something> accept instead the slice &[Something]. That's both lower power and more general. &Vec<T> automatically Derefs to &[T].

Copy link
Contributor

Choose a reason for hiding this comment

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

It could even be impl IntoIterator<Item = impl AsRef<Path>> to be really general, since all you do with this value is iterate over it.

Copy link
Member Author

Choose a reason for hiding this comment

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

impl feels a bit overkill. This doesn't expose any public interface, so it should be fine for it to not be polymorphic to make it easier to read.

The &[&Path] in this case sounds good to me!

Comment on lines 118 to 159
let check_result = if !attribute_info.is_derivation {
NixpkgsProblem::NonDerivation {
relative_package_file: relative_package_file.clone(),
package_name: package_name.clone(),
}
.into()
} else {
Success(())
};

let check_result = check_result.and(match &attribute_info.variant {
AttributeVariant::AutoCalled => Success(version::Attribute {
empty_non_auto_called: version::EmptyNonAutoCalled::Valid,
}),
AttributeVariant::CallPackage { path, empty_arg } => {
let correct_file = if let Some(call_package_path) = path {
absolute_package_file == *call_package_path
} else {
false
};
// Only check for the argument to be non-empty if the version is V1 or
// higher
let non_empty = if version >= Version::V1 {
!empty_arg
} else {
true
};
correct_file && non_empty
}
AttributeVariant::Other => false,
};

if !valid {
NixpkgsProblem::WrongCallPackage {
relative_package_file: relative_package_file.clone(),
package_name: package_name.clone(),
if correct_file {
Success(version::Attribute {
empty_non_auto_called: if *empty_arg {
version::EmptyNonAutoCalled::Invalid
} else {
version::EmptyNonAutoCalled::Valid
},
})
} else {
NixpkgsProblem::WrongCallPackage {
relative_package_file: relative_package_file.clone(),
package_name: package_name.clone(),
}
.into()
}
}
.into()
} else if !attribute_info.is_derivation {
NixpkgsProblem::NonDerivation {
AttributeVariant::Other => NixpkgsProblem::WrongCallPackage {
relative_package_file: relative_package_file.clone(),
package_name: package_name.clone(),
}
.into()
} else {
Success(())
}
.into(),
Copy link
Contributor

Choose a reason for hiding this comment

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

The current organization isn't bad at all and I would definitely accept it.

You could make a method on NixpkgsProblem for each condition that will be tested here, accepting whatever parameters are needed, and returning Option<Self> if the condition actually occured. That would make check_values look super sleek by sequencing together all the various checks, rather than bulky due to having the checks inlined.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah I would also lean towards splitting this up.

Copy link
Member Author

Choose a reason for hiding this comment

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

Sounds like an interesting idea, though I'd like to defer that to a future PR, since it will touch a bunch more parts of the code.

pkgs/test/nixpkgs-check-by-name/src/main.rs Outdated Show resolved Hide resolved
pkgs/test/nixpkgs-check-by-name/src/main.rs Outdated Show resolved Hide resolved
}
}

/// Checks whether the pkgs/by-name structure in Nixpkgs is valid,
/// and returns to which degree it's valid for checks with increased strictness.
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not sure what this sentence is trying to communicate.

Copy link
Member Author

Choose a reason for hiding this comment

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

pkgs/test/nixpkgs-check-by-name/src/main.rs Outdated Show resolved Hide resolved
Previously, not passing `--base` would enforce the most strict checks.
While there's currently no actual violation of these stricter checks,
this does not match the previous behavior.

This won't matter once CI passes `--base`, the code handling the
optionality can be removed then.
@infinisil
Copy link
Member Author

Thanks for the review! Greatly appreciated as I don't really consider myself a Rust expert ❤️

I addressed all the comments, let me know if it looks good to you!

I haven't had a chance to run the code or tests myself, which is the only reason I'm not signing off as-is.

Can confirm that the tests pass, at least on x86_64-linux (CI will take care of the rest). Note that if you want to run them yourself you can either nix-build -A tests.nixpkgs-check-by-name, or see the development section.

Copy link
Contributor

@philiptaron philiptaron left a comment

Choose a reason for hiding this comment

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

I'll give this an actual run later tonight.

Comment on lines 15 to 16
This allows the strictness of checks to increase over time by only preventing _new_ violations from being introduced,
while allowing violations that already existed.
Copy link
Contributor

Choose a reason for hiding this comment

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

At work, we call this a ratchet linter.

Copy link
Member Author

Choose a reason for hiding this comment

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

"Ratchet" is a great term for this idea, I updated the PR to use it! 79618ff

@infinisil
Copy link
Member Author

Finally, I moved all interface description into the --help output, so it's not duplicated: 74e8b38

Copy link
Contributor

@philiptaron philiptaron left a comment

Choose a reason for hiding this comment

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

🎉

Copy link
Contributor

@blaggacao blaggacao left a comment

Choose a reason for hiding this comment

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

The description of the first ratchet could be clearer. I think it's important to prioritize intelligibility so that people intuitively understand what they should not do.


The current ratchets are:

- If `pkgs.${name}` is not auto-called from `pkgs/by-name`, the `args` in its `callPackage` must not be empty,
Copy link
Contributor

Choose a reason for hiding this comment

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

That's hardly intelligible.

In part, due to that it's hard to observe an auto-called package, it's automatic after all. So how does a non-auto called look like?

I don't know what exactly is meant, but maybe could be:

It is not allowed to introduce new callPackage calls in top-level.nix, if those would have empty args.

Copy link
Member Author

Choose a reason for hiding this comment

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

While it's not strictly necessary for users to understand these checks (that's what CI is for!), it's still good for future developers. Pushed fc2d269 to improve this!

@infinisil infinisil merged commit 2a107bc into NixOS:master Dec 15, 2023
21 of 22 checks passed
@infinisil infinisil deleted the by-name-migrate-empty-arg branch December 15, 2023 18:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants