1+ use crate :: types:: Context ;
2+ use anyhow:: { anyhow, Result } ;
3+ use std:: collections:: HashSet ;
4+ use toml_edit:: { DocumentMut , Item } ;
5+
6+ #[ derive( Debug , clap:: Args ) ]
7+ /// Check that all Cargo.toml files have correct metadata and feature configuration
8+ pub struct Args ;
9+
10+ pub fn run ( ctx : & Context , _args : Args ) -> Result < ( ) > {
11+ let mut errors = Vec :: new ( ) ;
12+
13+ for ( crate_name, krate) in & ctx. crates {
14+ let cargo_toml_path = krate. path . join ( "Cargo.toml" ) ;
15+ let content = std:: fs:: read_to_string ( & cargo_toml_path)
16+ . map_err ( |e| anyhow ! ( "Failed to read {}: {}" , cargo_toml_path. display( ) , e) ) ?;
17+
18+ let doc: DocumentMut = content. parse ( )
19+ . map_err ( |e| anyhow ! ( "Failed to parse {}: {}" , cargo_toml_path. display( ) , e) ) ?;
20+
21+ // Check package metadata
22+ if let Err ( e) = check_package_metadata ( & doc, crate_name, krate. publish ) {
23+ errors. push ( format ! ( "{}: {}" , crate_name, e) ) ;
24+ }
25+
26+ // Check features - only for publishable crates
27+ if krate. publish {
28+ if let Err ( e) = check_features ( & doc) {
29+ errors. push ( format ! ( "{}: {}" , crate_name, e) ) ;
30+ }
31+ }
32+ }
33+
34+ if errors. is_empty ( ) {
35+ println ! ( "✅ All manifests are correct!" ) ;
36+ Ok ( ( ) )
37+ } else {
38+ for error in & errors {
39+ eprintln ! ( "❌ {}" , error) ;
40+ }
41+ Err ( anyhow ! ( "Found {} manifest errors" , errors. len( ) ) )
42+ }
43+ }
44+
45+ fn check_package_metadata ( doc : & DocumentMut , crate_name : & str , is_publishable : bool ) -> Result < ( ) > {
46+ let package = doc. get ( "package" )
47+ . ok_or_else ( || anyhow ! ( "missing [package] section" ) ) ?
48+ . as_table ( )
49+ . ok_or_else ( || anyhow ! ( "[package] is not a table" ) ) ?;
50+
51+ // Check license
52+ let license = package. get ( "license" )
53+ . ok_or_else ( || anyhow ! ( "missing license field" ) ) ?
54+ . as_str ( )
55+ . ok_or_else ( || anyhow ! ( "license field is not a string" ) ) ?;
56+
57+ if license != "MIT OR Apache-2.0" {
58+ return Err ( anyhow ! ( "license should be 'MIT OR Apache-2.0', found '{}'" , license) ) ;
59+ }
60+
61+ // Only check repository and documentation for publishable crates
62+ if is_publishable {
63+ // Check repository
64+ let repository = package. get ( "repository" )
65+ . ok_or_else ( || anyhow ! ( "missing repository field" ) ) ?
66+ . as_str ( )
67+ . ok_or_else ( || anyhow ! ( "repository field is not a string" ) ) ?;
68+
69+ if repository != "https://github.com/embassy-rs/embassy" {
70+ return Err ( anyhow ! ( "repository should be 'https://github.com/embassy-rs/embassy', found '{}'" , repository) ) ;
71+ }
72+
73+ // Check documentation
74+ let documentation = package. get ( "documentation" )
75+ . ok_or_else ( || anyhow ! ( "missing documentation field" ) ) ?
76+ . as_str ( )
77+ . ok_or_else ( || anyhow ! ( "documentation field is not a string" ) ) ?;
78+
79+ let expected_documentation = format ! ( "https://docs.embassy.dev/{}" , crate_name) ;
80+ if documentation != expected_documentation {
81+ return Err ( anyhow ! ( "documentation should be '{}', found '{}'" , expected_documentation, documentation) ) ;
82+ }
83+ }
84+
85+ Ok ( ( ) )
86+ }
87+
88+ fn check_features ( doc : & DocumentMut ) -> Result < ( ) > {
89+ // Get all optional dependencies
90+ let mut optional_deps: HashSet < String > = HashSet :: new ( ) ;
91+
92+ // Check dependencies
93+ if let Some ( deps) = doc. get ( "dependencies" ) . and_then ( |d| d. as_table ( ) ) {
94+ for ( name, value) in deps. iter ( ) {
95+ if is_optional_dependency ( value) {
96+ optional_deps. insert ( name. to_string ( ) ) ;
97+ }
98+ }
99+ }
100+
101+ // Check dev-dependencies
102+ if let Some ( deps) = doc. get ( "dev-dependencies" ) . and_then ( |d| d. as_table ( ) ) {
103+ for ( name, value) in deps. iter ( ) {
104+ if is_optional_dependency ( value) {
105+ optional_deps. insert ( name. to_string ( ) ) ;
106+ }
107+ }
108+ }
109+
110+ // Check build-dependencies
111+ if let Some ( deps) = doc. get ( "build-dependencies" ) . and_then ( |d| d. as_table ( ) ) {
112+ for ( name, value) in deps. iter ( ) {
113+ if is_optional_dependency ( value) {
114+ optional_deps. insert ( name. to_string ( ) ) ;
115+ }
116+ }
117+ }
118+
119+ if optional_deps. is_empty ( ) {
120+ return Ok ( ( ) ) ; // No optional dependencies to check
121+ }
122+
123+ // Get all features that reference dependencies
124+ let mut referenced_deps: HashSet < String > = HashSet :: new ( ) ;
125+
126+ if let Some ( features) = doc. get ( "features" ) . and_then ( |f| f. as_table ( ) ) {
127+ for ( _feature_name, feature_value) in features. iter ( ) {
128+ if let Some ( feature_list) = feature_value. as_array ( ) {
129+ for item in feature_list. iter ( ) {
130+ if let Some ( item_str) = item. as_str ( ) {
131+ if let Some ( dep_name) = item_str. strip_prefix ( "dep:" ) {
132+ referenced_deps. insert ( dep_name. to_string ( ) ) ;
133+ }
134+ }
135+ }
136+ }
137+ }
138+ }
139+
140+ // Find unreferenced optional dependencies
141+ let unreferenced: Vec < String > = optional_deps. difference ( & referenced_deps) . cloned ( ) . collect ( ) ;
142+
143+ if !unreferenced. is_empty ( ) {
144+ return Err ( anyhow ! (
145+ "optional dependencies not referenced by any feature with 'dep:': {}" ,
146+ unreferenced. join( ", " )
147+ ) ) ;
148+ }
149+
150+ Ok ( ( ) )
151+ }
152+
153+ fn is_optional_dependency ( value : & Item ) -> bool {
154+ match value {
155+ Item :: Value ( toml_edit:: Value :: InlineTable ( table) ) => {
156+ table. get ( "optional" )
157+ . and_then ( |v| v. as_bool ( ) )
158+ . unwrap_or ( false )
159+ }
160+ _ => false
161+ }
162+ }
0 commit comments