ci: Add lint to check if all sub-crates features can be enabled from ruma crate
This commit is contained in:
parent
30701596d0
commit
9b3f4a2c0f
@ -1,9 +1,14 @@
|
|||||||
use std::path::PathBuf;
|
#![allow(clippy::disallowed_types)]
|
||||||
|
|
||||||
|
use std::{collections::HashMap, path::PathBuf};
|
||||||
|
|
||||||
|
#[cfg(feature = "default")]
|
||||||
use reqwest::blocking::Client;
|
use reqwest::blocking::Client;
|
||||||
use semver::Version;
|
use semver::Version;
|
||||||
use serde::{de::IgnoredAny, Deserialize};
|
use serde::{de::IgnoredAny, Deserialize};
|
||||||
|
#[cfg(feature = "default")]
|
||||||
use toml_edit::{value, Document};
|
use toml_edit::{value, Document};
|
||||||
|
#[cfg(feature = "default")]
|
||||||
use xshell::{cmd, pushd, read_file, write_file};
|
use xshell::{cmd, pushd, read_file, write_file};
|
||||||
|
|
||||||
use crate::{util::ask_yes_no, Metadata, Result};
|
use crate::{util::ask_yes_no, Metadata, Result};
|
||||||
@ -22,11 +27,51 @@ pub struct Package {
|
|||||||
/// The package's manifest path.
|
/// The package's manifest path.
|
||||||
pub manifest_path: PathBuf,
|
pub manifest_path: PathBuf,
|
||||||
|
|
||||||
/// A map of the package dependencies.
|
/// A list of the package dependencies.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub dependencies: Vec<Dependency>,
|
pub dependencies: Vec<Dependency>,
|
||||||
|
|
||||||
|
/// A map of the package features.
|
||||||
|
#[serde(default)]
|
||||||
|
pub features: HashMap<String, Vec<String>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Package {
|
||||||
|
/// Whether this package has a way to enable the given feature from the given package.
|
||||||
|
pub fn can_enable_feature(&self, package_name: &str, feature_name: &str) -> bool {
|
||||||
|
for activated_feature in self.features.values().flatten() {
|
||||||
|
// Remove optional `dep:` at the start.
|
||||||
|
let remaining = activated_feature.trim_start_matches("dep:");
|
||||||
|
|
||||||
|
// Check that we have the package name.
|
||||||
|
let Some(remaining) = remaining.strip_prefix(package_name) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
if remaining.is_empty() {
|
||||||
|
// The feature only enables the dependency.
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove optional `?`.
|
||||||
|
let remaining = remaining.trim_start_matches('?');
|
||||||
|
|
||||||
|
let Some(remaining) = remaining.strip_prefix('/') else {
|
||||||
|
// This is another package name starting with the same string.
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Finally, only the feature name is remaining.
|
||||||
|
if remaining == feature_name {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "default")]
|
||||||
impl Package {
|
impl Package {
|
||||||
/// Update the version of this crate.
|
/// Update the version of this crate.
|
||||||
pub fn update_version(&mut self, version: &Version, dry_run: bool) -> Result<()> {
|
pub fn update_version(&mut self, version: &Version, dry_run: bool) -> Result<()> {
|
||||||
@ -203,6 +248,7 @@ pub enum DependencyKind {
|
|||||||
Build,
|
Build,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "default")]
|
||||||
/// A crate from the `GET /crates/{crate}` endpoint of crates.io.
|
/// A crate from the `GET /crates/{crate}` endpoint of crates.io.
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
struct CratesIoCrate {
|
struct CratesIoCrate {
|
||||||
|
@ -1,15 +1,17 @@
|
|||||||
// Triggers at the `#[clap(subcommand)]` line, but not easily reproducible outside this crate.
|
// Triggers at the `#[clap(subcommand)]` line, but not easily reproducible outside this crate.
|
||||||
#![allow(unused_qualifications)]
|
#![allow(unused_qualifications)]
|
||||||
|
|
||||||
use std::path::PathBuf;
|
use std::path::Path;
|
||||||
|
|
||||||
use clap::{Args, Subcommand};
|
use clap::{Args, Subcommand};
|
||||||
use xshell::pushd;
|
use xshell::pushd;
|
||||||
|
|
||||||
use crate::{cmd, Metadata, Result, NIGHTLY};
|
use crate::{cmd, Metadata, Result, NIGHTLY};
|
||||||
|
|
||||||
|
mod reexport_features;
|
||||||
mod spec_links;
|
mod spec_links;
|
||||||
|
|
||||||
|
use reexport_features::check_reexport_features;
|
||||||
use spec_links::check_spec_links;
|
use spec_links::check_spec_links;
|
||||||
|
|
||||||
const MSRV: &str = "1.75";
|
const MSRV: &str = "1.75";
|
||||||
@ -66,6 +68,8 @@ pub enum CiCmd {
|
|||||||
Dependencies,
|
Dependencies,
|
||||||
/// Check spec links point to a recent version (lint)
|
/// Check spec links point to a recent version (lint)
|
||||||
SpecLinks,
|
SpecLinks,
|
||||||
|
/// Check all cargo features of sub-crates can be enabled from ruma (lint)
|
||||||
|
ReexportFeatures,
|
||||||
/// Check typos
|
/// Check typos
|
||||||
Typos,
|
Typos,
|
||||||
}
|
}
|
||||||
@ -75,18 +79,22 @@ pub struct CiTask {
|
|||||||
/// Which command to run.
|
/// Which command to run.
|
||||||
cmd: Option<CiCmd>,
|
cmd: Option<CiCmd>,
|
||||||
|
|
||||||
/// The root of the workspace.
|
/// The metadata of the workspace.
|
||||||
project_root: PathBuf,
|
project_metadata: Metadata,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CiTask {
|
impl CiTask {
|
||||||
pub(crate) fn new(cmd: Option<CiCmd>) -> Result<Self> {
|
pub(crate) fn new(cmd: Option<CiCmd>) -> Result<Self> {
|
||||||
let project_root = Metadata::load()?.workspace_root;
|
let project_metadata = Metadata::load()?;
|
||||||
Ok(Self { cmd, project_root })
|
Ok(Self { cmd, project_metadata })
|
||||||
|
}
|
||||||
|
|
||||||
|
fn project_root(&self) -> &Path {
|
||||||
|
&self.project_metadata.workspace_root
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn run(self) -> Result<()> {
|
pub(crate) fn run(self) -> Result<()> {
|
||||||
let _p = pushd(&self.project_root)?;
|
let _p = pushd(self.project_root())?;
|
||||||
|
|
||||||
match self.cmd {
|
match self.cmd {
|
||||||
Some(CiCmd::Msrv) => self.msrv()?,
|
Some(CiCmd::Msrv) => self.msrv()?,
|
||||||
@ -110,7 +118,8 @@ impl CiTask {
|
|||||||
Some(CiCmd::ClippyAll) => self.clippy_all()?,
|
Some(CiCmd::ClippyAll) => self.clippy_all()?,
|
||||||
Some(CiCmd::Lint) => self.lint()?,
|
Some(CiCmd::Lint) => self.lint()?,
|
||||||
Some(CiCmd::Dependencies) => self.dependencies()?,
|
Some(CiCmd::Dependencies) => self.dependencies()?,
|
||||||
Some(CiCmd::SpecLinks) => check_spec_links(&self.project_root.join("crates"))?,
|
Some(CiCmd::SpecLinks) => check_spec_links(&self.project_root().join("crates"))?,
|
||||||
|
Some(CiCmd::ReexportFeatures) => check_reexport_features(&self.project_metadata)?,
|
||||||
Some(CiCmd::Typos) => self.typos()?,
|
Some(CiCmd::Typos) => self.typos()?,
|
||||||
None => {
|
None => {
|
||||||
self.msrv()
|
self.msrv()
|
||||||
@ -301,9 +310,11 @@ impl CiTask {
|
|||||||
// Check dependencies being sorted
|
// Check dependencies being sorted
|
||||||
let dependencies_res = self.dependencies();
|
let dependencies_res = self.dependencies();
|
||||||
// Check that all links point to the same version of the spec
|
// Check that all links point to the same version of the spec
|
||||||
let spec_links_res = check_spec_links(&self.project_root.join("crates"));
|
let spec_links_res = check_spec_links(&self.project_root().join("crates"));
|
||||||
|
// Check that all cargo features of sub-crates can be enabled from ruma.
|
||||||
|
let reexport_features_res = check_reexport_features(&self.project_metadata);
|
||||||
|
|
||||||
dependencies_res.and(spec_links_res)
|
dependencies_res.and(spec_links_res).and(reexport_features_res)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check the sorting of dependencies with the nightly version.
|
/// Check the sorting of dependencies with the nightly version.
|
||||||
|
54
xtask/src/ci/reexport_features.rs
Normal file
54
xtask/src/ci/reexport_features.rs
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
use crate::{Metadata, Result};
|
||||||
|
|
||||||
|
/// Check that the ruma crate allows to enable all the features of the other ruma-* crates.
|
||||||
|
///
|
||||||
|
/// For simplicity, this function assumes that:
|
||||||
|
///
|
||||||
|
/// - Those dependencies are not renamed.
|
||||||
|
/// - ruma does not use `default-features = false` on those dependencies.
|
||||||
|
///
|
||||||
|
/// This does not check if all features are re-exported individually, as that is not always wanted.
|
||||||
|
pub(crate) fn check_reexport_features(metadata: &Metadata) -> Result<()> {
|
||||||
|
println!("Checking all features can be enabled from ruma…");
|
||||||
|
let mut n_errors = 0;
|
||||||
|
|
||||||
|
let Some(ruma) = metadata.find_package("ruma") else {
|
||||||
|
return Err("ruma package not found in workspace".into());
|
||||||
|
};
|
||||||
|
|
||||||
|
for package in ruma.dependencies.iter().filter_map(|dep| metadata.find_package(&dep.name)) {
|
||||||
|
println!("Checking features of {}…", package.name);
|
||||||
|
|
||||||
|
// Exclude ruma and xtask.
|
||||||
|
if !package.name.starts_with("ruma-") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter features that are enabled by other features of the same package.
|
||||||
|
let features = package.features.keys().filter(|feature_name| {
|
||||||
|
!package.features.values().flatten().any(|activated_feature| {
|
||||||
|
activated_feature.trim_start_matches("dep:") == *feature_name
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
for feature_name in features {
|
||||||
|
// Let's assume that ruma never has `default-features = false`.
|
||||||
|
if feature_name == "default" {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if !ruma.can_enable_feature(&package.name, feature_name) {
|
||||||
|
println!(r#" Missing feature "{}/{feature_name}""#, package.name);
|
||||||
|
n_errors += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if n_errors > 0 {
|
||||||
|
// Visual aid to separate the end error message.
|
||||||
|
println!();
|
||||||
|
return Err(format!("Found {n_errors} missing features").into());
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
@ -17,7 +17,6 @@ use serde_json::from_str as from_json_str;
|
|||||||
// Keep in sync with version in `rust-toolchain.toml` and `.github/workflows/ci.yml`
|
// Keep in sync with version in `rust-toolchain.toml` and `.github/workflows/ci.yml`
|
||||||
const NIGHTLY: &str = "nightly-2024-02-14";
|
const NIGHTLY: &str = "nightly-2024-02-14";
|
||||||
|
|
||||||
#[cfg(feature = "default")]
|
|
||||||
mod cargo;
|
mod cargo;
|
||||||
mod ci;
|
mod ci;
|
||||||
mod doc;
|
mod doc;
|
||||||
@ -26,6 +25,7 @@ mod release;
|
|||||||
#[cfg(feature = "default")]
|
#[cfg(feature = "default")]
|
||||||
mod util;
|
mod util;
|
||||||
|
|
||||||
|
use cargo::Package;
|
||||||
use ci::{CiArgs, CiTask};
|
use ci::{CiArgs, CiTask};
|
||||||
use doc::DocTask;
|
use doc::DocTask;
|
||||||
#[cfg(feature = "default")]
|
#[cfg(feature = "default")]
|
||||||
@ -70,8 +70,7 @@ fn main() -> Result<()> {
|
|||||||
#[derive(Clone, Debug, Deserialize)]
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
struct Metadata {
|
struct Metadata {
|
||||||
pub workspace_root: PathBuf,
|
pub workspace_root: PathBuf,
|
||||||
#[cfg(feature = "default")]
|
pub packages: Vec<Package>,
|
||||||
pub packages: Vec<cargo::Package>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Metadata {
|
impl Metadata {
|
||||||
@ -80,6 +79,11 @@ impl Metadata {
|
|||||||
let metadata_json = cmd!("cargo metadata --no-deps --format-version 1").read()?;
|
let metadata_json = cmd!("cargo metadata --no-deps --format-version 1").read()?;
|
||||||
Ok(from_json_str(&metadata_json)?)
|
Ok(from_json_str(&metadata_json)?)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Find the package with the given name.
|
||||||
|
pub fn find_package(&self, name: &str) -> Option<&Package> {
|
||||||
|
self.packages.iter().find(|p| p.name == name)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "default")]
|
#[cfg(feature = "default")]
|
||||||
|
Loading…
x
Reference in New Issue
Block a user