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 semver::Version;
 | 
			
		||||
use serde::{de::IgnoredAny, Deserialize};
 | 
			
		||||
#[cfg(feature = "default")]
 | 
			
		||||
use toml_edit::{value, Document};
 | 
			
		||||
#[cfg(feature = "default")]
 | 
			
		||||
use xshell::{cmd, pushd, read_file, write_file};
 | 
			
		||||
 | 
			
		||||
use crate::{util::ask_yes_no, Metadata, Result};
 | 
			
		||||
@ -22,11 +27,51 @@ pub struct Package {
 | 
			
		||||
    /// The package's manifest path.
 | 
			
		||||
    pub manifest_path: PathBuf,
 | 
			
		||||
 | 
			
		||||
    /// A map of the package dependencies.
 | 
			
		||||
    /// A list of the package dependencies.
 | 
			
		||||
    #[serde(default)]
 | 
			
		||||
    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 {
 | 
			
		||||
    /// Update the version of this crate.
 | 
			
		||||
    pub fn update_version(&mut self, version: &Version, dry_run: bool) -> Result<()> {
 | 
			
		||||
@ -203,6 +248,7 @@ pub enum DependencyKind {
 | 
			
		||||
    Build,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[cfg(feature = "default")]
 | 
			
		||||
/// A crate from the `GET /crates/{crate}` endpoint of crates.io.
 | 
			
		||||
#[derive(Deserialize)]
 | 
			
		||||
struct CratesIoCrate {
 | 
			
		||||
 | 
			
		||||
@ -1,15 +1,17 @@
 | 
			
		||||
// Triggers at the `#[clap(subcommand)]` line, but not easily reproducible outside this crate.
 | 
			
		||||
#![allow(unused_qualifications)]
 | 
			
		||||
 | 
			
		||||
use std::path::PathBuf;
 | 
			
		||||
use std::path::Path;
 | 
			
		||||
 | 
			
		||||
use clap::{Args, Subcommand};
 | 
			
		||||
use xshell::pushd;
 | 
			
		||||
 | 
			
		||||
use crate::{cmd, Metadata, Result, NIGHTLY};
 | 
			
		||||
 | 
			
		||||
mod reexport_features;
 | 
			
		||||
mod spec_links;
 | 
			
		||||
 | 
			
		||||
use reexport_features::check_reexport_features;
 | 
			
		||||
use spec_links::check_spec_links;
 | 
			
		||||
 | 
			
		||||
const MSRV: &str = "1.75";
 | 
			
		||||
@ -66,6 +68,8 @@ pub enum CiCmd {
 | 
			
		||||
    Dependencies,
 | 
			
		||||
    /// Check spec links point to a recent version (lint)
 | 
			
		||||
    SpecLinks,
 | 
			
		||||
    /// Check all cargo features of sub-crates can be enabled from ruma (lint)
 | 
			
		||||
    ReexportFeatures,
 | 
			
		||||
    /// Check typos
 | 
			
		||||
    Typos,
 | 
			
		||||
}
 | 
			
		||||
@ -75,18 +79,22 @@ pub struct CiTask {
 | 
			
		||||
    /// Which command to run.
 | 
			
		||||
    cmd: Option<CiCmd>,
 | 
			
		||||
 | 
			
		||||
    /// The root of the workspace.
 | 
			
		||||
    project_root: PathBuf,
 | 
			
		||||
    /// The metadata of the workspace.
 | 
			
		||||
    project_metadata: Metadata,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl CiTask {
 | 
			
		||||
    pub(crate) fn new(cmd: Option<CiCmd>) -> Result<Self> {
 | 
			
		||||
        let project_root = Metadata::load()?.workspace_root;
 | 
			
		||||
        Ok(Self { cmd, project_root })
 | 
			
		||||
        let project_metadata = Metadata::load()?;
 | 
			
		||||
        Ok(Self { cmd, project_metadata })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn project_root(&self) -> &Path {
 | 
			
		||||
        &self.project_metadata.workspace_root
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub(crate) fn run(self) -> Result<()> {
 | 
			
		||||
        let _p = pushd(&self.project_root)?;
 | 
			
		||||
        let _p = pushd(self.project_root())?;
 | 
			
		||||
 | 
			
		||||
        match self.cmd {
 | 
			
		||||
            Some(CiCmd::Msrv) => self.msrv()?,
 | 
			
		||||
@ -110,7 +118,8 @@ impl CiTask {
 | 
			
		||||
            Some(CiCmd::ClippyAll) => self.clippy_all()?,
 | 
			
		||||
            Some(CiCmd::Lint) => self.lint()?,
 | 
			
		||||
            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()?,
 | 
			
		||||
            None => {
 | 
			
		||||
                self.msrv()
 | 
			
		||||
@ -301,9 +310,11 @@ impl CiTask {
 | 
			
		||||
        // Check dependencies being sorted
 | 
			
		||||
        let dependencies_res = self.dependencies();
 | 
			
		||||
        // 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.
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										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`
 | 
			
		||||
const NIGHTLY: &str = "nightly-2024-02-14";
 | 
			
		||||
 | 
			
		||||
#[cfg(feature = "default")]
 | 
			
		||||
mod cargo;
 | 
			
		||||
mod ci;
 | 
			
		||||
mod doc;
 | 
			
		||||
@ -26,6 +25,7 @@ mod release;
 | 
			
		||||
#[cfg(feature = "default")]
 | 
			
		||||
mod util;
 | 
			
		||||
 | 
			
		||||
use cargo::Package;
 | 
			
		||||
use ci::{CiArgs, CiTask};
 | 
			
		||||
use doc::DocTask;
 | 
			
		||||
#[cfg(feature = "default")]
 | 
			
		||||
@ -70,8 +70,7 @@ fn main() -> Result<()> {
 | 
			
		||||
#[derive(Clone, Debug, Deserialize)]
 | 
			
		||||
struct Metadata {
 | 
			
		||||
    pub workspace_root: PathBuf,
 | 
			
		||||
    #[cfg(feature = "default")]
 | 
			
		||||
    pub packages: Vec<cargo::Package>,
 | 
			
		||||
    pub packages: Vec<Package>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Metadata {
 | 
			
		||||
@ -80,6 +79,11 @@ impl Metadata {
 | 
			
		||||
        let metadata_json = cmd!("cargo metadata --no-deps --format-version 1").read()?;
 | 
			
		||||
        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")]
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user