tests.nixpkgs-check-by-name: Apply feedback from review
https://github.com/NixOS/nixpkgs/pull/285089#pullrequestreview-1861099233 Many thanks, Philip Taron!
This commit is contained in:
parent
7d1f0a4cd4
commit
37a54e3ad5
7 changed files with 252 additions and 279 deletions
|
@ -15,7 +15,7 @@ lazy_static = "1.4.0"
|
||||||
colored = "2.0.4"
|
colored = "2.0.4"
|
||||||
itertools = "0.11.0"
|
itertools = "0.11.0"
|
||||||
rowan = "0.15.11"
|
rowan = "0.15.11"
|
||||||
indoc = "2.0.4"
|
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
temp-env = "0.3.5"
|
temp-env = "0.3.5"
|
||||||
|
indoc = "2.0.4"
|
||||||
|
|
|
@ -2,9 +2,9 @@ use crate::nixpkgs_problem::NixpkgsProblem;
|
||||||
use crate::ratchet;
|
use crate::ratchet;
|
||||||
use crate::structure;
|
use crate::structure;
|
||||||
use crate::utils;
|
use crate::utils;
|
||||||
use crate::validation::ResultIteratorExt;
|
use crate::validation::ResultIteratorExt as _;
|
||||||
use crate::validation::{self, Validation::Success};
|
use crate::validation::{self, Validation::Success};
|
||||||
use crate::NixFileCache;
|
use crate::NixFileStore;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
|
@ -82,7 +82,7 @@ enum CallPackageVariant {
|
||||||
/// See the `eval.nix` file for how this is achieved on the Nix side
|
/// See the `eval.nix` file for how this is achieved on the Nix side
|
||||||
pub fn check_values(
|
pub fn check_values(
|
||||||
nixpkgs_path: &Path,
|
nixpkgs_path: &Path,
|
||||||
nix_file_cache: &mut NixFileCache,
|
nix_file_store: &mut NixFileStore,
|
||||||
package_names: Vec<String>,
|
package_names: Vec<String>,
|
||||||
keep_nix_path: bool,
|
keep_nix_path: bool,
|
||||||
) -> validation::Result<ratchet::Nixpkgs> {
|
) -> validation::Result<ratchet::Nixpkgs> {
|
||||||
|
@ -155,77 +155,77 @@ pub fn check_values(
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let check_result = validation::sequence(attributes.into_iter().map(
|
let check_result = validation::sequence(
|
||||||
|(attribute_name, attribute_value)| {
|
attributes
|
||||||
let relative_package_file = structure::relative_file_for_package(&attribute_name);
|
.into_iter()
|
||||||
|
.map(|(attribute_name, attribute_value)| {
|
||||||
|
let relative_package_file = structure::relative_file_for_package(&attribute_name);
|
||||||
|
|
||||||
use ratchet::RatchetState::*;
|
use ratchet::RatchetState::*;
|
||||||
use Attribute::*;
|
use Attribute::*;
|
||||||
use AttributeInfo::*;
|
use AttributeInfo::*;
|
||||||
use ByNameAttribute::*;
|
use ByNameAttribute::*;
|
||||||
use CallPackageVariant::*;
|
use CallPackageVariant::*;
|
||||||
use NonByNameAttribute::*;
|
use NonByNameAttribute::*;
|
||||||
|
|
||||||
let check_result = match attribute_value {
|
let check_result = match attribute_value {
|
||||||
// The attribute succeeds evaluation and is NOT defined in pkgs/by-name
|
// The attribute succeeds evaluation and is NOT defined in pkgs/by-name
|
||||||
NonByName(EvalSuccess(attribute_info)) => {
|
NonByName(EvalSuccess(attribute_info)) => {
|
||||||
let uses_by_name = match attribute_info {
|
let uses_by_name = match attribute_info {
|
||||||
// In these cases the package doesn't qualify for being in pkgs/by-name,
|
// In these cases the package doesn't qualify for being in pkgs/by-name,
|
||||||
// so the UsesByName ratchet is already as tight as it can be
|
// so the UsesByName ratchet is already as tight as it can be
|
||||||
NonAttributeSet => Success(NonApplicable),
|
NonAttributeSet => Success(NonApplicable),
|
||||||
NonCallPackage => Success(NonApplicable),
|
NonCallPackage => Success(NonApplicable),
|
||||||
// This is the case when the `pkgs/by-name`-internal _internalCallByNamePackageFile
|
// This is the case when the `pkgs/by-name`-internal _internalCallByNamePackageFile
|
||||||
// is used for a package outside `pkgs/by-name`
|
// is used for a package outside `pkgs/by-name`
|
||||||
CallPackage(CallPackageInfo {
|
CallPackage(CallPackageInfo {
|
||||||
call_package_variant: Auto,
|
call_package_variant: Auto,
|
||||||
..
|
..
|
||||||
}) => {
|
}) => {
|
||||||
// With the current detection mechanism, this also triggers for aliases
|
// With the current detection mechanism, this also triggers for aliases
|
||||||
// to pkgs/by-name packages, and there's no good method of
|
// to pkgs/by-name packages, and there's no good method of
|
||||||
// distinguishing alias vs non-alias.
|
// distinguishing alias vs non-alias.
|
||||||
// Using `config.allowAliases = false` at least currently doesn't work
|
// Using `config.allowAliases = false` at least currently doesn't work
|
||||||
// because there's nothing preventing people from defining aliases that
|
// because there's nothing preventing people from defining aliases that
|
||||||
// are present even with that disabled.
|
// are present even with that disabled.
|
||||||
// In the future we could kind of abuse this behavior to have better
|
// In the future we could kind of abuse this behavior to have better
|
||||||
// enforcement of conditional aliases, but for now we just need to not
|
// enforcement of conditional aliases, but for now we just need to not
|
||||||
// give an error.
|
// give an error.
|
||||||
Success(NonApplicable)
|
Success(NonApplicable)
|
||||||
}
|
}
|
||||||
// Only derivations can be in pkgs/by-name,
|
// Only derivations can be in pkgs/by-name,
|
||||||
// so this attribute doesn't qualify
|
// so this attribute doesn't qualify
|
||||||
CallPackage(CallPackageInfo {
|
CallPackage(CallPackageInfo {
|
||||||
is_derivation: false,
|
is_derivation: false,
|
||||||
..
|
..
|
||||||
}) => Success(NonApplicable),
|
}) => Success(NonApplicable),
|
||||||
// A location of None indicates something weird, we can't really know where
|
// A location of None indicates something weird, we can't really know where
|
||||||
// this attribute is defined, probably an alias
|
// this attribute is defined, probably an alias
|
||||||
CallPackage(CallPackageInfo { location: None, .. }) => Success(Tight),
|
CallPackage(CallPackageInfo { location: None, .. }) => Success(Tight),
|
||||||
// The case of an attribute that qualifies:
|
// The case of an attribute that qualifies:
|
||||||
// - Uses callPackage
|
// - Uses callPackage
|
||||||
// - Is a derivation
|
// - Is a derivation
|
||||||
CallPackage(CallPackageInfo {
|
CallPackage(CallPackageInfo {
|
||||||
is_derivation: true,
|
is_derivation: true,
|
||||||
call_package_variant: Manual { .. },
|
call_package_variant: Manual { .. },
|
||||||
location: Some(location),
|
location: Some(location),
|
||||||
}) =>
|
}) =>
|
||||||
// We'll use the attribute's location to parse the file that defines it
|
// We'll use the attribute's location to parse the file that defines it
|
||||||
match nix_file_cache.get(&location.file)? {
|
{
|
||||||
Err(error) =>
|
match nix_file_store
|
||||||
// This is a bad sign for rnix, because it means cpp Nix could parse
|
.get(&location.file)?
|
||||||
// something that rnix couldn't
|
.call_package_argument_info_at(
|
||||||
anyhow::bail!(
|
location.line,
|
||||||
"Could not parse file {} with rnix, even though it parsed with cpp Nix: {error}",
|
location.column,
|
||||||
location.file.display()
|
nixpkgs_path,
|
||||||
),
|
)? {
|
||||||
Ok(nix_file) =>
|
// If the definition is not of the form `<attr> = callPackage <arg1> <arg2>;`,
|
||||||
match nix_file.call_package_argument_info_at(
|
// it's generally not possible to migrate to `pkgs/by-name`
|
||||||
location.line,
|
|
||||||
location.column,
|
|
||||||
nixpkgs_path,
|
|
||||||
)? {
|
|
||||||
None => Success(NonApplicable),
|
None => Success(NonApplicable),
|
||||||
Some(call_package_argument_info) => {
|
Some(call_package_argument_info) => {
|
||||||
if let Some(ref rel_path) = call_package_argument_info.relative_path {
|
if let Some(ref rel_path) =
|
||||||
|
call_package_argument_info.relative_path
|
||||||
|
{
|
||||||
if rel_path.starts_with(utils::BASE_SUBPATH) {
|
if rel_path.starts_with(utils::BASE_SUBPATH) {
|
||||||
// Package variants of by-name packages are explicitly allowed according to RFC 140
|
// Package variants of by-name packages are explicitly allowed according to RFC 140
|
||||||
// https://github.com/NixOS/rfcs/blob/master/rfcs/0140-simple-package-paths.md#package-variants:
|
// https://github.com/NixOS/rfcs/blob/master/rfcs/0140-simple-package-paths.md#package-variants:
|
||||||
|
@ -244,100 +244,104 @@ pub fn check_values(
|
||||||
} else {
|
} else {
|
||||||
Success(Loose(call_package_argument_info))
|
Success(Loose(call_package_argument_info))
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
uses_by_name.map(|x| ratchet::Package {
|
|
||||||
manual_definition: Tight,
|
|
||||||
uses_by_name: x,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
NonByName(EvalFailure) => {
|
|
||||||
// We don't know anything about this attribute really
|
|
||||||
Success(ratchet::Package {
|
|
||||||
// We'll assume that we can't remove any manual definitions, which has the
|
|
||||||
// minimal drawback that if there was a manual definition that could've
|
|
||||||
// been removed, fixing the package requires removing the definition, no
|
|
||||||
// big deal, that's a minor edit.
|
|
||||||
manual_definition: Tight,
|
|
||||||
|
|
||||||
// Regarding whether this attribute could `pkgs/by-name`, we don't really
|
|
||||||
// know, so return NonApplicable, which has the effect that if a
|
|
||||||
// package evaluation gets broken temporarily, the fix can remove it from
|
|
||||||
// pkgs/by-name again. For now this isn't our problem, but in the future we
|
|
||||||
// might have another check to enforce that evaluation must not be broken.
|
|
||||||
// The alternative of assuming that it's using `pkgs/by-name` already
|
|
||||||
// has the problem that if a package evaluation gets broken temporarily,
|
|
||||||
// fixing it requires a move to pkgs/by-name, which could happen more
|
|
||||||
// often and isn't really justified.
|
|
||||||
uses_by_name: NonApplicable,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
ByName(Missing) => NixpkgsProblem::UndefinedAttr {
|
|
||||||
relative_package_file: relative_package_file.clone(),
|
|
||||||
package_name: attribute_name.clone(),
|
|
||||||
}
|
|
||||||
.into(),
|
|
||||||
ByName(Existing(NonAttributeSet)) => NixpkgsProblem::NonDerivation {
|
|
||||||
relative_package_file: relative_package_file.clone(),
|
|
||||||
package_name: attribute_name.clone(),
|
|
||||||
}
|
|
||||||
.into(),
|
|
||||||
ByName(Existing(NonCallPackage)) => NixpkgsProblem::WrongCallPackage {
|
|
||||||
relative_package_file: relative_package_file.clone(),
|
|
||||||
package_name: attribute_name.clone(),
|
|
||||||
}
|
|
||||||
.into(),
|
|
||||||
ByName(Existing(CallPackage(CallPackageInfo {
|
|
||||||
is_derivation,
|
|
||||||
call_package_variant,
|
|
||||||
..
|
|
||||||
}))) => {
|
|
||||||
let check_result = if !is_derivation {
|
|
||||||
NixpkgsProblem::NonDerivation {
|
|
||||||
relative_package_file: relative_package_file.clone(),
|
|
||||||
package_name: attribute_name.clone(),
|
|
||||||
}
|
|
||||||
.into()
|
|
||||||
} else {
|
|
||||||
Success(())
|
|
||||||
};
|
|
||||||
|
|
||||||
check_result.and(match &call_package_variant {
|
|
||||||
Auto => Success(ratchet::Package {
|
|
||||||
manual_definition: Tight,
|
|
||||||
uses_by_name: Tight,
|
|
||||||
}),
|
|
||||||
// TODO: Use the call_package_argument_info_at instead/additionally and
|
|
||||||
// simplify the eval.nix code
|
|
||||||
Manual { path, empty_arg } => {
|
|
||||||
let correct_file = if let Some(call_package_path) = path {
|
|
||||||
relative_package_file == *call_package_path
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
};
|
|
||||||
|
|
||||||
if correct_file {
|
|
||||||
Success(ratchet::Package {
|
|
||||||
// Empty arguments for non-auto-called packages are not allowed anymore.
|
|
||||||
manual_definition: if *empty_arg { Loose(()) } else { Tight },
|
|
||||||
uses_by_name: Tight,
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
NixpkgsProblem::WrongCallPackage {
|
|
||||||
relative_package_file: relative_package_file.clone(),
|
|
||||||
package_name: attribute_name.clone(),
|
|
||||||
}
|
}
|
||||||
.into()
|
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
})
|
uses_by_name.map(|x| ratchet::Package {
|
||||||
}
|
manual_definition: Tight,
|
||||||
};
|
uses_by_name: x,
|
||||||
Ok::<_, anyhow::Error>(check_result.map(|value| (attribute_name.clone(), value)))
|
})
|
||||||
})
|
}
|
||||||
.collect_vec()?,
|
NonByName(EvalFailure) => {
|
||||||
|
// We don't know anything about this attribute really
|
||||||
|
Success(ratchet::Package {
|
||||||
|
// We'll assume that we can't remove any manual definitions, which has the
|
||||||
|
// minimal drawback that if there was a manual definition that could've
|
||||||
|
// been removed, fixing the package requires removing the definition, no
|
||||||
|
// big deal, that's a minor edit.
|
||||||
|
manual_definition: Tight,
|
||||||
|
|
||||||
|
// Regarding whether this attribute could `pkgs/by-name`, we don't really
|
||||||
|
// know, so return NonApplicable, which has the effect that if a
|
||||||
|
// package evaluation gets broken temporarily, the fix can remove it from
|
||||||
|
// pkgs/by-name again. For now this isn't our problem, but in the future we
|
||||||
|
// might have another check to enforce that evaluation must not be broken.
|
||||||
|
// The alternative of assuming that it's using `pkgs/by-name` already
|
||||||
|
// has the problem that if a package evaluation gets broken temporarily,
|
||||||
|
// fixing it requires a move to pkgs/by-name, which could happen more
|
||||||
|
// often and isn't really justified.
|
||||||
|
uses_by_name: NonApplicable,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
ByName(Missing) => NixpkgsProblem::UndefinedAttr {
|
||||||
|
relative_package_file: relative_package_file.clone(),
|
||||||
|
package_name: attribute_name.clone(),
|
||||||
|
}
|
||||||
|
.into(),
|
||||||
|
ByName(Existing(NonAttributeSet)) => NixpkgsProblem::NonDerivation {
|
||||||
|
relative_package_file: relative_package_file.clone(),
|
||||||
|
package_name: attribute_name.clone(),
|
||||||
|
}
|
||||||
|
.into(),
|
||||||
|
ByName(Existing(NonCallPackage)) => NixpkgsProblem::WrongCallPackage {
|
||||||
|
relative_package_file: relative_package_file.clone(),
|
||||||
|
package_name: attribute_name.clone(),
|
||||||
|
}
|
||||||
|
.into(),
|
||||||
|
ByName(Existing(CallPackage(CallPackageInfo {
|
||||||
|
is_derivation,
|
||||||
|
call_package_variant,
|
||||||
|
..
|
||||||
|
}))) => {
|
||||||
|
let check_result = if !is_derivation {
|
||||||
|
NixpkgsProblem::NonDerivation {
|
||||||
|
relative_package_file: relative_package_file.clone(),
|
||||||
|
package_name: attribute_name.clone(),
|
||||||
|
}
|
||||||
|
.into()
|
||||||
|
} else {
|
||||||
|
Success(())
|
||||||
|
};
|
||||||
|
|
||||||
|
check_result.and(match &call_package_variant {
|
||||||
|
Auto => Success(ratchet::Package {
|
||||||
|
manual_definition: Tight,
|
||||||
|
uses_by_name: Tight,
|
||||||
|
}),
|
||||||
|
// TODO: Use the call_package_argument_info_at instead/additionally and
|
||||||
|
// simplify the eval.nix code
|
||||||
|
Manual { path, empty_arg } => {
|
||||||
|
let correct_file = if let Some(call_package_path) = path {
|
||||||
|
relative_package_file == *call_package_path
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
};
|
||||||
|
|
||||||
|
if correct_file {
|
||||||
|
Success(ratchet::Package {
|
||||||
|
// Empty arguments for non-auto-called packages are not allowed anymore.
|
||||||
|
manual_definition: if *empty_arg {
|
||||||
|
Loose(())
|
||||||
|
} else {
|
||||||
|
Tight
|
||||||
|
},
|
||||||
|
uses_by_name: Tight,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
NixpkgsProblem::WrongCallPackage {
|
||||||
|
relative_package_file: relative_package_file.clone(),
|
||||||
|
package_name: attribute_name.clone(),
|
||||||
|
}
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Ok::<_, anyhow::Error>(check_result.map(|value| (attribute_name.clone(), value)))
|
||||||
|
})
|
||||||
|
.collect_vec()?,
|
||||||
);
|
);
|
||||||
|
|
||||||
Ok(check_result.map(|elems| ratchet::Nixpkgs {
|
Ok(check_result.map(|elems| ratchet::Nixpkgs {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use crate::nix_file::NixFileCache;
|
use crate::nix_file::NixFileStore;
|
||||||
mod eval;
|
mod eval;
|
||||||
mod nix_file;
|
mod nix_file;
|
||||||
mod nixpkgs_problem;
|
mod nixpkgs_problem;
|
||||||
|
@ -118,7 +118,7 @@ pub fn check_nixpkgs<W: io::Write>(
|
||||||
keep_nix_path: bool,
|
keep_nix_path: bool,
|
||||||
error_writer: &mut W,
|
error_writer: &mut W,
|
||||||
) -> validation::Result<ratchet::Nixpkgs> {
|
) -> validation::Result<ratchet::Nixpkgs> {
|
||||||
let mut nix_file_cache = NixFileCache::new();
|
let mut nix_file_store = NixFileStore::default();
|
||||||
|
|
||||||
Ok({
|
Ok({
|
||||||
let nixpkgs_path = nixpkgs_path.canonicalize().with_context(|| {
|
let nixpkgs_path = nixpkgs_path.canonicalize().with_context(|| {
|
||||||
|
@ -136,9 +136,9 @@ pub fn check_nixpkgs<W: io::Write>(
|
||||||
)?;
|
)?;
|
||||||
Success(ratchet::Nixpkgs::default())
|
Success(ratchet::Nixpkgs::default())
|
||||||
} else {
|
} else {
|
||||||
check_structure(&nixpkgs_path, &mut nix_file_cache)?.result_map(|package_names|
|
check_structure(&nixpkgs_path, &mut nix_file_store)?.result_map(|package_names|
|
||||||
// Only if we could successfully parse the structure, we do the evaluation checks
|
// Only if we could successfully parse the structure, we do the evaluation checks
|
||||||
eval::check_values(&nixpkgs_path, &mut nix_file_cache, package_names, keep_nix_path))?
|
eval::check_values(&nixpkgs_path, &mut nix_file_store, package_names, keep_nix_path))?
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,26 +15,20 @@ use std::fs::read_to_string;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
/// A structure to indefinitely cache parse results of Nix files in memory,
|
/// A structure to store parse results of Nix files in memory,
|
||||||
/// making sure that the same file never has to be parsed twice
|
/// making sure that the same file never has to be parsed twice
|
||||||
pub struct NixFileCache {
|
#[derive(Default)]
|
||||||
entries: HashMap<PathBuf, NixFileResult>,
|
pub struct NixFileStore {
|
||||||
|
entries: HashMap<PathBuf, NixFile>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl NixFileCache {
|
impl NixFileStore {
|
||||||
/// Creates a new empty NixFileCache
|
/// Get the store entry for a Nix file if it exists, otherwise parse the file, insert it into
|
||||||
pub fn new() -> NixFileCache {
|
/// the store, and return the value
|
||||||
NixFileCache {
|
|
||||||
entries: HashMap::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the cache entry for a Nix file if it exists, otherwise parse the file, insert it into
|
|
||||||
/// the cache, and return the value
|
|
||||||
///
|
///
|
||||||
/// Note that this function only gives an anyhow::Result::Err for I/O errors.
|
/// Note that this function only gives an anyhow::Result::Err for I/O errors.
|
||||||
/// A parse error is anyhow::Result::Ok(Result::Err(error))
|
/// A parse error is anyhow::Result::Ok(Result::Err(error))
|
||||||
pub fn get(&mut self, path: &Path) -> anyhow::Result<&NixFileResult> {
|
pub fn get(&mut self, path: &Path) -> anyhow::Result<&NixFile> {
|
||||||
match self.entries.entry(path.to_owned()) {
|
match self.entries.entry(path.to_owned()) {
|
||||||
Entry::Occupied(entry) => Ok(entry.into_mut()),
|
Entry::Occupied(entry) => Ok(entry.into_mut()),
|
||||||
Entry::Vacant(entry) => Ok(entry.insert(NixFile::new(path)?)),
|
Entry::Vacant(entry) => Ok(entry.insert(NixFile::new(path)?)),
|
||||||
|
@ -42,9 +36,6 @@ impl NixFileCache {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A type to represent the result when trying to initialise a `NixFile`.
|
|
||||||
type NixFileResult = Result<NixFile, rnix::parser::ParseError>;
|
|
||||||
|
|
||||||
/// A structure for storing a successfully parsed Nix file
|
/// A structure for storing a successfully parsed Nix file
|
||||||
pub struct NixFile {
|
pub struct NixFile {
|
||||||
/// The parent directory of the Nix file, for more convenient error handling
|
/// The parent directory of the Nix file, for more convenient error handling
|
||||||
|
@ -57,7 +48,7 @@ pub struct NixFile {
|
||||||
|
|
||||||
impl NixFile {
|
impl NixFile {
|
||||||
/// Creates a new NixFile, failing for I/O or parse errors
|
/// Creates a new NixFile, failing for I/O or parse errors
|
||||||
fn new(path: impl AsRef<Path>) -> anyhow::Result<NixFileResult> {
|
fn new(path: impl AsRef<Path>) -> anyhow::Result<NixFile> {
|
||||||
let Some(parent_dir) = path.as_ref().parent() else {
|
let Some(parent_dir) = path.as_ref().parent() else {
|
||||||
anyhow::bail!("Could not get parent of path {}", path.as_ref().display())
|
anyhow::bail!("Could not get parent of path {}", path.as_ref().display())
|
||||||
};
|
};
|
||||||
|
@ -66,14 +57,21 @@ impl NixFile {
|
||||||
.with_context(|| format!("Could not read file {}", path.as_ref().display()))?;
|
.with_context(|| format!("Could not read file {}", path.as_ref().display()))?;
|
||||||
let line_index = LineIndex::new(&contents);
|
let line_index = LineIndex::new(&contents);
|
||||||
|
|
||||||
Ok(rnix::Root::parse(&contents)
|
// NOTE: There's now another Nixpkgs CI check to make sure all changed Nix files parse
|
||||||
|
// correctly, though that uses mainline Nix instead of rnix, so it doesn't give the same
|
||||||
|
// errors. In the future we should unify these two checks, ideally moving the other CI
|
||||||
|
// check into this tool as well and checking for both mainline Nix and rnix.
|
||||||
|
rnix::Root::parse(&contents)
|
||||||
|
// rnix's ::ok returns Result<_, _> , so no error is thrown away like it would be with
|
||||||
|
// std::result's ::ok
|
||||||
.ok()
|
.ok()
|
||||||
.map(|syntax_root| NixFile {
|
.map(|syntax_root| NixFile {
|
||||||
parent_dir: parent_dir.to_path_buf(),
|
parent_dir: parent_dir.to_path_buf(),
|
||||||
path: path.as_ref().to_owned(),
|
path: path.as_ref().to_owned(),
|
||||||
syntax_root,
|
syntax_root,
|
||||||
line_index,
|
line_index,
|
||||||
}))
|
})
|
||||||
|
.with_context(|| format!("Could not parse file {} with rnix", path.as_ref().display()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -88,7 +86,11 @@ pub struct CallPackageArgumentInfo {
|
||||||
|
|
||||||
impl NixFile {
|
impl NixFile {
|
||||||
/// Returns information about callPackage arguments for an attribute at a specific line/column
|
/// Returns information about callPackage arguments for an attribute at a specific line/column
|
||||||
/// index. If there's no guaranteed `callPackage` at that location, `None` is returned.
|
/// index.
|
||||||
|
/// If the location is not of the form `<attr> = callPackage <arg1> <arg2>;`, `None` is
|
||||||
|
/// returned.
|
||||||
|
/// This function only returns `Err` for problems that can't be caused by the Nix contents,
|
||||||
|
/// but rather problems in this programs code itself.
|
||||||
///
|
///
|
||||||
/// This is meant to be used with the location returned from `builtins.unsafeGetAttrPos`, e.g.:
|
/// This is meant to be used with the location returned from `builtins.unsafeGetAttrPos`, e.g.:
|
||||||
/// - Create file `default.nix` with contents
|
/// - Create file `default.nix` with contents
|
||||||
|
@ -102,7 +104,7 @@ impl NixFile {
|
||||||
/// builtins.unsafeGetAttrPos "foo" (import ./default.nix { })
|
/// builtins.unsafeGetAttrPos "foo" (import ./default.nix { })
|
||||||
/// ```
|
/// ```
|
||||||
/// results in `{ file = ./default.nix; line = 2; column = 3; }`
|
/// results in `{ file = ./default.nix; line = 2; column = 3; }`
|
||||||
/// - Get the NixFile for `.file` from a `NixFileCache`
|
/// - Get the NixFile for `.file` from a `NixFileStore`
|
||||||
/// - Call this function with `.line`, `.column` and `relative_to` as the (absolute) current directory
|
/// - Call this function with `.line`, `.column` and `relative_to` as the (absolute) current directory
|
||||||
///
|
///
|
||||||
/// You'll get back
|
/// You'll get back
|
||||||
|
@ -165,6 +167,7 @@ impl NixFile {
|
||||||
};
|
};
|
||||||
|
|
||||||
if attrpath_node.kind() != SyntaxKind::NODE_ATTRPATH {
|
if attrpath_node.kind() != SyntaxKind::NODE_ATTRPATH {
|
||||||
|
// This can happen for e.g. `inherit foo`, so definitely not a syntactic `callPackage`
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
// attrpath_node looks like "foo.bar"
|
// attrpath_node looks like "foo.bar"
|
||||||
|
@ -183,7 +186,9 @@ impl NixFile {
|
||||||
}
|
}
|
||||||
// attrpath_value_node looks like "foo.bar = 10;"
|
// attrpath_value_node looks like "foo.bar = 10;"
|
||||||
|
|
||||||
// unwrap is fine because we confirmed that we can cast with the above check
|
// unwrap is fine because we confirmed that we can cast with the above check.
|
||||||
|
// We could avoid this `unwrap` for a `clone`, since `cast` consumes the argument,
|
||||||
|
// but we still need it for the error message when the cast fails.
|
||||||
Ok(Some(ast::AttrpathValue::cast(attrpath_value_node).unwrap()))
|
Ok(Some(ast::AttrpathValue::cast(attrpath_value_node).unwrap()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -272,9 +277,8 @@ impl NixFile {
|
||||||
|
|
||||||
// Check that <fun2> is an identifier, or an attribute path with an identifier at the end
|
// Check that <fun2> is an identifier, or an attribute path with an identifier at the end
|
||||||
let ident = match function2 {
|
let ident = match function2 {
|
||||||
Expr::Ident(ident) =>
|
Expr::Ident(ident) => {
|
||||||
// This means it's something like `foo = callPackage <arg2> <arg1>`
|
// This means it's something like `foo = callPackage <arg2> <arg1>`
|
||||||
{
|
|
||||||
ident
|
ident
|
||||||
}
|
}
|
||||||
Expr::Select(select) => {
|
Expr::Select(select) => {
|
||||||
|
@ -340,13 +344,12 @@ pub enum ResolvedPath {
|
||||||
|
|
||||||
impl NixFile {
|
impl NixFile {
|
||||||
/// Statically resolves a Nix path expression and checks that it's within an absolute path
|
/// Statically resolves a Nix path expression and checks that it's within an absolute path
|
||||||
/// path
|
|
||||||
///
|
///
|
||||||
/// E.g. for the path expression `./bar.nix` in `./foo.nix` and an absolute path of the
|
/// E.g. for the path expression `./bar.nix` in `./foo.nix` and an absolute path of the
|
||||||
/// current directory, the function returns `ResolvedPath::Within(./bar.nix)`
|
/// current directory, the function returns `ResolvedPath::Within(./bar.nix)`
|
||||||
pub fn static_resolve_path(&self, node: ast::Path, relative_to: &Path) -> ResolvedPath {
|
pub fn static_resolve_path(&self, node: ast::Path, relative_to: &Path) -> ResolvedPath {
|
||||||
if node.parts().count() != 1 {
|
if node.parts().count() != 1 {
|
||||||
// Only if there's more than 1 interpolated part, it's of the form `./foo/${bar}/baz`.
|
// If there's more than 1 interpolated part, it's of the form `./foo/${bar}/baz`.
|
||||||
return ResolvedPath::Interpolated;
|
return ResolvedPath::Interpolated;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -364,9 +367,8 @@ impl NixFile {
|
||||||
// That should be checked more strictly
|
// That should be checked more strictly
|
||||||
match self.parent_dir.join(Path::new(&text)).canonicalize() {
|
match self.parent_dir.join(Path::new(&text)).canonicalize() {
|
||||||
Err(resolution_error) => ResolvedPath::Unresolvable(resolution_error),
|
Err(resolution_error) => ResolvedPath::Unresolvable(resolution_error),
|
||||||
Ok(resolved) =>
|
Ok(resolved) => {
|
||||||
// Check if it's within relative_to
|
// Check if it's within relative_to
|
||||||
{
|
|
||||||
match resolved.strip_prefix(relative_to) {
|
match resolved.strip_prefix(relative_to) {
|
||||||
Err(_prefix_error) => ResolvedPath::Outside,
|
Err(_prefix_error) => ResolvedPath::Outside,
|
||||||
Ok(suffix) => ResolvedPath::Within(suffix.to_path_buf()),
|
Ok(suffix) => ResolvedPath::Within(suffix.to_path_buf()),
|
||||||
|
@ -411,7 +413,7 @@ mod tests {
|
||||||
|
|
||||||
std::fs::write(&file, contents)?;
|
std::fs::write(&file, contents)?;
|
||||||
|
|
||||||
let nix_file = NixFile::new(&file)??;
|
let nix_file = NixFile::new(&file)?;
|
||||||
|
|
||||||
// These are builtins.unsafeGetAttrPos locations for the attributes
|
// These are builtins.unsafeGetAttrPos locations for the attributes
|
||||||
let cases = [
|
let cases = [
|
||||||
|
@ -454,7 +456,7 @@ mod tests {
|
||||||
|
|
||||||
std::fs::write(&file, contents)?;
|
std::fs::write(&file, contents)?;
|
||||||
|
|
||||||
let nix_file = NixFile::new(&file)??;
|
let nix_file = NixFile::new(&file)?;
|
||||||
|
|
||||||
let cases = [
|
let cases = [
|
||||||
(2, None),
|
(2, None),
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
use crate::structure;
|
use crate::structure;
|
||||||
use crate::utils::PACKAGE_NIX_FILENAME;
|
use crate::utils::PACKAGE_NIX_FILENAME;
|
||||||
use rnix::parser::ParseError;
|
|
||||||
use std::ffi::OsString;
|
use std::ffi::OsString;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::io;
|
use std::io;
|
||||||
|
@ -58,11 +57,6 @@ pub enum NixpkgsProblem {
|
||||||
subpath: PathBuf,
|
subpath: PathBuf,
|
||||||
io_error: io::Error,
|
io_error: io::Error,
|
||||||
},
|
},
|
||||||
CouldNotParseNix {
|
|
||||||
relative_package_dir: PathBuf,
|
|
||||||
subpath: PathBuf,
|
|
||||||
error: ParseError,
|
|
||||||
},
|
|
||||||
PathInterpolation {
|
PathInterpolation {
|
||||||
relative_package_dir: PathBuf,
|
relative_package_dir: PathBuf,
|
||||||
subpath: PathBuf,
|
subpath: PathBuf,
|
||||||
|
@ -184,14 +178,6 @@ impl fmt::Display for NixpkgsProblem {
|
||||||
relative_package_dir.display(),
|
relative_package_dir.display(),
|
||||||
subpath.display(),
|
subpath.display(),
|
||||||
),
|
),
|
||||||
NixpkgsProblem::CouldNotParseNix { relative_package_dir, subpath, error } =>
|
|
||||||
write!(
|
|
||||||
f,
|
|
||||||
"{}: File {} could not be parsed by rnix: {}",
|
|
||||||
relative_package_dir.display(),
|
|
||||||
subpath.display(),
|
|
||||||
error,
|
|
||||||
),
|
|
||||||
NixpkgsProblem::PathInterpolation { relative_package_dir, subpath, line, text } =>
|
NixpkgsProblem::PathInterpolation { relative_package_dir, subpath, line, text } =>
|
||||||
write!(
|
write!(
|
||||||
f,
|
f,
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
use crate::nixpkgs_problem::NixpkgsProblem;
|
use crate::nixpkgs_problem::NixpkgsProblem;
|
||||||
use crate::utils;
|
use crate::utils;
|
||||||
use crate::validation::{self, ResultIteratorExt, Validation::Success};
|
use crate::validation::{self, ResultIteratorExt, Validation::Success};
|
||||||
use crate::NixFileCache;
|
use crate::NixFileStore;
|
||||||
|
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
use rowan::ast::AstNode;
|
use rowan::ast::AstNode;
|
||||||
|
@ -11,17 +11,20 @@ use std::path::Path;
|
||||||
/// Check that every package directory in pkgs/by-name doesn't link to outside that directory.
|
/// Check that every package directory in pkgs/by-name doesn't link to outside that directory.
|
||||||
/// Both symlinks and Nix path expressions are checked.
|
/// Both symlinks and Nix path expressions are checked.
|
||||||
pub fn check_references(
|
pub fn check_references(
|
||||||
nix_file_cache: &mut NixFileCache,
|
nix_file_store: &mut NixFileStore,
|
||||||
relative_package_dir: &Path,
|
relative_package_dir: &Path,
|
||||||
absolute_package_dir: &Path,
|
absolute_package_dir: &Path,
|
||||||
) -> validation::Result<()> {
|
) -> validation::Result<()> {
|
||||||
// The empty argument here is the subpath under the package directory to check
|
// The first subpath to check is the package directory itself, which we can represent as an
|
||||||
// An empty one means the package directory itself
|
// empty path, since the absolute package directory gets prepended to this.
|
||||||
|
// We don't use `./.` to keep the error messages cleaner
|
||||||
|
// (there's no canonicalisation going on underneath)
|
||||||
|
let subpath = Path::new("");
|
||||||
check_path(
|
check_path(
|
||||||
nix_file_cache,
|
nix_file_store,
|
||||||
relative_package_dir,
|
relative_package_dir,
|
||||||
absolute_package_dir,
|
absolute_package_dir,
|
||||||
Path::new(""),
|
subpath,
|
||||||
)
|
)
|
||||||
.with_context(|| {
|
.with_context(|| {
|
||||||
format!(
|
format!(
|
||||||
|
@ -32,8 +35,12 @@ pub fn check_references(
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Checks for a specific path to not have references outside
|
/// Checks for a specific path to not have references outside
|
||||||
|
///
|
||||||
|
/// The subpath is the relative path within the package directory we're currently checking.
|
||||||
|
/// A relative path so that the error messages don't get absolute paths (which are messy in CI).
|
||||||
|
/// The absolute package directory gets prepended before doing anything with it though.
|
||||||
fn check_path(
|
fn check_path(
|
||||||
nix_file_cache: &mut NixFileCache,
|
nix_file_store: &mut NixFileStore,
|
||||||
relative_package_dir: &Path,
|
relative_package_dir: &Path,
|
||||||
absolute_package_dir: &Path,
|
absolute_package_dir: &Path,
|
||||||
subpath: &Path,
|
subpath: &Path,
|
||||||
|
@ -69,23 +76,22 @@ fn check_path(
|
||||||
utils::read_dir_sorted(&path)?
|
utils::read_dir_sorted(&path)?
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|entry| {
|
.map(|entry| {
|
||||||
let entry_subpath = subpath.join(entry.file_name());
|
|
||||||
check_path(
|
check_path(
|
||||||
nix_file_cache,
|
nix_file_store,
|
||||||
relative_package_dir,
|
relative_package_dir,
|
||||||
absolute_package_dir,
|
absolute_package_dir,
|
||||||
&entry_subpath,
|
&subpath.join(entry.file_name()),
|
||||||
)
|
)
|
||||||
.with_context(|| format!("Error while recursing into {}", subpath.display()))
|
|
||||||
})
|
})
|
||||||
.collect_vec()?,
|
.collect_vec()
|
||||||
|
.with_context(|| format!("Error while recursing into {}", subpath.display()))?,
|
||||||
)
|
)
|
||||||
} else if path.is_file() {
|
} else if path.is_file() {
|
||||||
// Only check Nix files
|
// Only check Nix files
|
||||||
if let Some(ext) = path.extension() {
|
if let Some(ext) = path.extension() {
|
||||||
if ext == OsStr::new("nix") {
|
if ext == OsStr::new("nix") {
|
||||||
check_nix_file(
|
check_nix_file(
|
||||||
nix_file_cache,
|
nix_file_store,
|
||||||
relative_package_dir,
|
relative_package_dir,
|
||||||
absolute_package_dir,
|
absolute_package_dir,
|
||||||
subpath,
|
subpath,
|
||||||
|
@ -106,29 +112,14 @@ fn check_path(
|
||||||
/// Check whether a nix file contains path expression references pointing outside the package
|
/// Check whether a nix file contains path expression references pointing outside the package
|
||||||
/// directory
|
/// directory
|
||||||
fn check_nix_file(
|
fn check_nix_file(
|
||||||
nix_file_cache: &mut NixFileCache,
|
nix_file_store: &mut NixFileStore,
|
||||||
relative_package_dir: &Path,
|
relative_package_dir: &Path,
|
||||||
absolute_package_dir: &Path,
|
absolute_package_dir: &Path,
|
||||||
subpath: &Path,
|
subpath: &Path,
|
||||||
) -> validation::Result<()> {
|
) -> validation::Result<()> {
|
||||||
let path = absolute_package_dir.join(subpath);
|
let path = absolute_package_dir.join(subpath);
|
||||||
|
|
||||||
let nix_file = match nix_file_cache.get(&path)? {
|
let nix_file = nix_file_store.get(&path)?;
|
||||||
Ok(nix_file) => nix_file,
|
|
||||||
Err(error) =>
|
|
||||||
// NOTE: There's now another Nixpkgs CI check to make sure all changed Nix files parse
|
|
||||||
// correctly, though that uses mainline Nix instead of rnix, so it doesn't give the same
|
|
||||||
// errors. In the future we should unify these two checks, ideally moving the other CI
|
|
||||||
// check into this tool as well and checking for both mainline Nix and rnix.
|
|
||||||
{
|
|
||||||
return Ok(NixpkgsProblem::CouldNotParseNix {
|
|
||||||
relative_package_dir: relative_package_dir.to_path_buf(),
|
|
||||||
subpath: subpath.to_path_buf(),
|
|
||||||
error: error.clone(),
|
|
||||||
}
|
|
||||||
.into())
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(validation::sequence_(
|
Ok(validation::sequence_(
|
||||||
nix_file.syntax_root.syntax().descendants().map(|node| {
|
nix_file.syntax_root.syntax().descendants().map(|node| {
|
||||||
|
@ -140,40 +131,31 @@ fn check_nix_file(
|
||||||
return Success(());
|
return Success(());
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::nix_file::ResolvedPath::*;
|
use crate::nix_file::ResolvedPath;
|
||||||
|
|
||||||
match nix_file.static_resolve_path(path, absolute_package_dir) {
|
match nix_file.static_resolve_path(path, absolute_package_dir) {
|
||||||
Interpolated =>
|
ResolvedPath::Interpolated => NixpkgsProblem::PathInterpolation {
|
||||||
// Filters out ./foo/${bar}/baz
|
|
||||||
// TODO: We can just check ./foo
|
|
||||||
{
|
|
||||||
NixpkgsProblem::PathInterpolation {
|
|
||||||
relative_package_dir: relative_package_dir.to_path_buf(),
|
|
||||||
subpath: subpath.to_path_buf(),
|
|
||||||
line,
|
|
||||||
text,
|
|
||||||
}
|
|
||||||
.into()
|
|
||||||
}
|
|
||||||
SearchPath =>
|
|
||||||
// Filters out search paths like <nixpkgs>
|
|
||||||
{
|
|
||||||
NixpkgsProblem::SearchPath {
|
|
||||||
relative_package_dir: relative_package_dir.to_path_buf(),
|
|
||||||
subpath: subpath.to_path_buf(),
|
|
||||||
line,
|
|
||||||
text,
|
|
||||||
}
|
|
||||||
.into()
|
|
||||||
}
|
|
||||||
Outside => NixpkgsProblem::OutsidePathReference {
|
|
||||||
relative_package_dir: relative_package_dir.to_path_buf(),
|
relative_package_dir: relative_package_dir.to_path_buf(),
|
||||||
subpath: subpath.to_path_buf(),
|
subpath: subpath.to_path_buf(),
|
||||||
line,
|
line,
|
||||||
text,
|
text,
|
||||||
}
|
}
|
||||||
.into(),
|
.into(),
|
||||||
Unresolvable(e) => NixpkgsProblem::UnresolvablePathReference {
|
ResolvedPath::SearchPath => NixpkgsProblem::SearchPath {
|
||||||
|
relative_package_dir: relative_package_dir.to_path_buf(),
|
||||||
|
subpath: subpath.to_path_buf(),
|
||||||
|
line,
|
||||||
|
text,
|
||||||
|
}
|
||||||
|
.into(),
|
||||||
|
ResolvedPath::Outside => NixpkgsProblem::OutsidePathReference {
|
||||||
|
relative_package_dir: relative_package_dir.to_path_buf(),
|
||||||
|
subpath: subpath.to_path_buf(),
|
||||||
|
line,
|
||||||
|
text,
|
||||||
|
}
|
||||||
|
.into(),
|
||||||
|
ResolvedPath::Unresolvable(e) => NixpkgsProblem::UnresolvablePathReference {
|
||||||
relative_package_dir: relative_package_dir.to_path_buf(),
|
relative_package_dir: relative_package_dir.to_path_buf(),
|
||||||
subpath: subpath.to_path_buf(),
|
subpath: subpath.to_path_buf(),
|
||||||
line,
|
line,
|
||||||
|
@ -181,10 +163,9 @@ fn check_nix_file(
|
||||||
io_error: e,
|
io_error: e,
|
||||||
}
|
}
|
||||||
.into(),
|
.into(),
|
||||||
Within(_p) =>
|
ResolvedPath::Within(..) => {
|
||||||
// No need to handle the case of it being inside the directory, since we scan through the
|
// No need to handle the case of it being inside the directory, since we scan through the
|
||||||
// entire directory recursively anyways
|
// entire directory recursively anyways
|
||||||
{
|
|
||||||
Success(())
|
Success(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@ use crate::references;
|
||||||
use crate::utils;
|
use crate::utils;
|
||||||
use crate::utils::{BASE_SUBPATH, PACKAGE_NIX_FILENAME};
|
use crate::utils::{BASE_SUBPATH, PACKAGE_NIX_FILENAME};
|
||||||
use crate::validation::{self, ResultIteratorExt, Validation::Success};
|
use crate::validation::{self, ResultIteratorExt, Validation::Success};
|
||||||
use crate::NixFileCache;
|
use crate::NixFileStore;
|
||||||
use itertools::concat;
|
use itertools::concat;
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
|
@ -37,7 +37,7 @@ pub fn relative_file_for_package(package_name: &str) -> PathBuf {
|
||||||
/// `pkgs/by-name`
|
/// `pkgs/by-name`
|
||||||
pub fn check_structure(
|
pub fn check_structure(
|
||||||
path: &Path,
|
path: &Path,
|
||||||
nix_file_cache: &mut NixFileCache,
|
nix_file_store: &mut NixFileStore,
|
||||||
) -> validation::Result<Vec<String>> {
|
) -> validation::Result<Vec<String>> {
|
||||||
let base_dir = path.join(BASE_SUBPATH);
|
let base_dir = path.join(BASE_SUBPATH);
|
||||||
|
|
||||||
|
@ -93,7 +93,7 @@ pub fn check_structure(
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|package_entry| {
|
.map(|package_entry| {
|
||||||
check_package(
|
check_package(
|
||||||
nix_file_cache,
|
nix_file_store,
|
||||||
path,
|
path,
|
||||||
&shard_name,
|
&shard_name,
|
||||||
shard_name_valid,
|
shard_name_valid,
|
||||||
|
@ -112,7 +112,7 @@ pub fn check_structure(
|
||||||
}
|
}
|
||||||
|
|
||||||
fn check_package(
|
fn check_package(
|
||||||
nix_file_cache: &mut NixFileCache,
|
nix_file_store: &mut NixFileStore,
|
||||||
path: &Path,
|
path: &Path,
|
||||||
shard_name: &str,
|
shard_name: &str,
|
||||||
shard_name_valid: bool,
|
shard_name_valid: bool,
|
||||||
|
@ -172,7 +172,7 @@ fn check_package(
|
||||||
});
|
});
|
||||||
|
|
||||||
let result = result.and(references::check_references(
|
let result = result.and(references::check_references(
|
||||||
nix_file_cache,
|
nix_file_store,
|
||||||
&relative_package_dir,
|
&relative_package_dir,
|
||||||
&path.join(&relative_package_dir),
|
&path.join(&relative_package_dir),
|
||||||
)?);
|
)?);
|
||||||
|
|
Loading…
Reference in a new issue