inwelling/
lib.rs

1// Copyright 2018 oooutlk@outlook.com. See the COPYRIGHT
2// file at the top-level directory of this distribution.
3//
4// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
5// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
6// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
7// option. This file may not be copied, modified, or distributed
8// except according to those terms.
9
10//! # Project Goal
11//!
12//! To provide a mechanism for upstream crates to collect information from
13//! downstream crates.
14//!
15//! # Information collected from downstream crates
16//!
17//! Invoking `collect_downstream()` will collect the following information from
18//! crates which called `inwelling::to()` in its `build.rs`.
19//!
20//! - Package name.
21//!
22//! - Metadata defined in `Cargo.toml`.
23//!
24//! - Manifest paths of `Cargo.toml`.
25//!
26//! - Source file paths(optional). Call `collect_downstream()` with the argument
27//! `inwelling::Opt::dump_rs_paths == true` to collect.
28//!
29//! # Quickstart
30//!
31//! 1. The upstream crate e.g. `crate foo` calls `inwelling::collect_downstream()`
32//!    in its `build.rs` and do whatever it want to generate APIs for downstream.
33//!
34//! 2. The downstream crate e.g. `crate bar` calls `inwelling::to()` in its
35//!    `build.rs`.
36//!
37//!    ```rust,no_run
38//!    // build.rs
39//!    fn main() { inwelling::to( "foo" ); }
40//!    ```
41//!
42//!    To send some metadata to upstream `crate foo`, encode them in `Cargo.toml`'s
43//!    package metadata.
44//!
45//!    ```toml
46//!    [package.metadata.inwelling.foo]
47//!    answer = { type = "integer", value = "42" }
48//!    ```
49
50use std::{
51    collections::{HashMap, HashSet},
52    env,
53    fs::{self, File},
54    io::Write,
55    path::{Path, PathBuf},
56    thread,
57    time::Duration,
58};
59
60use walkdir::WalkDir;
61
62/// Information collected from downstream crates.
63#[derive( Debug )]
64pub struct Downstream {
65    pub packages : Vec<Package>,
66}
67
68impl Default for Downstream {
69    fn default() -> Self {
70        Downstream{ packages: Vec::new() }
71    }
72}
73
74/// Information collected from one downstream crate. Including:
75///
76/// - Package name.
77///
78/// - Cargo.toml file' path.
79///
80/// - metadata from `[package.metadata.inwelling.*]` section in Cargo.toml file.
81///
82/// - Optional .rs file paths.
83#[derive( Debug )]
84pub struct Package {
85    /// name of the package which called `inwelling::to()` in its `build.rs`.
86    pub name     : String,
87    /// path of `Cargo.toml`.
88    pub manifest : PathBuf,
89    /// metadata represented in Toml.
90    pub metadata : toml::Value,
91    /// .rs files under src/, examples/ and tests/ directories if `dump_rs_file`
92    /// is true, otherwise `None`.
93    pub rs_paths : Option<Vec<PathBuf>>,
94}
95
96fn scan_rs_paths( current_dir: impl AsRef<Path>, rs_paths: &mut Vec<PathBuf> ) {
97    if let Ok( entries ) = current_dir.as_ref().read_dir() {
98        for entry in entries {
99            if let Ok( entry ) = entry {
100                let path = entry.path();
101                if path.is_dir() {
102                    scan_rs_paths( path, rs_paths );
103                } else if let Some( extention ) = path.extension() {
104                    if extention == "rs" {
105                        rs_paths.push( path );
106                    }
107                }
108            }
109        }
110    }
111}
112
113/// Options passed to inwelling().
114pub struct Opts {
115    /// build.rs using inwelling() will re-run if downstream crates' Cargo.toml files have been changed.
116    pub watch_manifest : bool,
117    /// build.rs using inwelling() will re-run if downstream crates' .rs files have been changed.
118    pub watch_rs_files : bool,
119    /// if this flag is true, inwelling()'s returning value will contain .rs file paths.
120    pub dump_rs_paths  : bool,
121}
122
123impl Default for Opts {
124    fn default() -> Opts {
125        Opts {
126            watch_manifest : true,
127            watch_rs_files : false,
128            dump_rs_paths  : false,
129        }
130    }
131}
132
133/// Collects information from downstream crates. Including:
134///
135/// - Package names.
136///
137/// - Cargo.toml files' paths.
138///
139/// - metadata from `[package.metadata.inwelling.*]` sections in Cargo.toml files.
140///
141/// - Optional .rs file paths.
142pub fn collect_downstream( Opts{ watch_manifest, watch_rs_files, dump_rs_paths }: Opts ) -> Downstream {
143    let build_name = env::var("CARGO_PKG_NAME").expect("CARGO_PKG_NAME");
144
145    let manifest_paths = locate_manifest_paths();
146
147    manifest_paths.into_iter().fold( Downstream::default(), |mut inwelling, (manifest_path, upstreams)| {
148        if upstreams.contains( &build_name ) {
149            let cargo_toml =
150                fs::read_to_string( PathBuf::from( &manifest_path ))
151                .expect( &format!( "to read {:?}", manifest_path ))
152                .parse::<toml::Table>()
153                .expect( &format!( "{:?} should be a valid manifest", manifest_path ));
154            let package = cargo_toml.get( "package" )
155                .expect( &format!( "{:?} should contain '[package]' section", manifest_path ));
156            let package_name = package.as_table()
157                .expect( &format!( "[package] section in {:?} should contain key-value pair(s)", manifest_path ))
158                .get( "name" )
159                .expect( &format!( "{:?} should contain package name", manifest_path ))
160                .as_str()
161                .expect( &format!( "{:?}'s package name should be a string", manifest_path ))
162                .to_owned();
163
164            let mut rs_paths = Vec::new();
165
166            if watch_manifest {
167                println!( "cargo:rerun-if-changed={}", manifest_path.to_str().unwrap() );
168            }
169            if dump_rs_paths || watch_rs_files {
170                let manifest_dir = manifest_path.parent().unwrap();
171                scan_rs_paths( &manifest_dir.join( "src"      ), &mut rs_paths );
172                scan_rs_paths( &manifest_dir.join( "examples" ), &mut rs_paths );
173                scan_rs_paths( &manifest_dir.join( "tests"    ), &mut rs_paths );
174                if watch_rs_files {
175                    rs_paths.iter().for_each( |rs_file|
176                        println!( "cargo:rerun-if-changed={}", rs_file.to_str().unwrap() ));
177                }
178            }
179            if let Some( metadata ) = package.get( "metadata" ) {
180                if let Some( metadata_inwelling ) = metadata.get("inwelling") {
181                    if let Some( metadata_inwelling_build ) = metadata_inwelling.get( &build_name ) {
182                        inwelling.packages.push( Package{
183                            name     : package_name,
184                            manifest : manifest_path,
185                            metadata : metadata_inwelling_build.clone(),
186                            rs_paths : if dump_rs_paths { Some( rs_paths )} else { None },
187                        });
188                    }
189                }
190            }
191        }
192
193        inwelling
194    })
195}
196
197// the path of the file that stores the downstream crate's manifest directory.
198const MANIFEST_DIR_INWELLING: &'static str = "manifest_dir.inwelling";
199
200fn wait_for_other_builds( build_dir: &Path ) {
201    let mut generated = HashSet::<PathBuf>::new();
202    let mut waiting = true;
203    while waiting {
204        thread::sleep( Duration::from_secs(5) );
205        waiting = false;
206        for entry in WalkDir::new( build_dir ) {
207            let entry = entry.unwrap();
208            let path = entry.path();
209            if generated.insert( path.to_owned() ) {
210                waiting = true;
211            }
212        }
213    }
214    eprintln!("{generated:#?}");
215}
216
217fn locate_manifest_paths() -> HashMap<PathBuf,Vec<String>> {
218    let mut path_bufs = HashMap::new();
219
220    let out_dir = PathBuf::from( env::var( "OUT_DIR" ).expect( "$OUT_DIR should exist." ));
221    let ancestors = out_dir.ancestors();
222    let build_dir = ancestors.skip(2).next().expect( "'build' directory should exist." );
223
224    wait_for_other_builds( &build_dir );
225
226    let mut pending = true;
227    while pending {
228        pending = false;
229        for entry in build_dir.read_dir().expect( &format!( "to list all sub dirs in {:?}", build_dir )) {
230            if let Ok( entry ) = entry {
231                let path = entry.path();
232                if path.is_dir() {
233                    let inwelling_file_path = path.join("out").join( MANIFEST_DIR_INWELLING );
234                    if inwelling_file_path.exists() {
235                        let contents = fs::read_to_string( &inwelling_file_path )
236                            .expect( &format!( "to read {:?} to get one manifest path", inwelling_file_path ));
237                        let mut lines = contents.lines();
238                        let manifest_dir = lines.next()
239                            .expect( &format!( "{:?} should contain the line of manifest dir.", inwelling_file_path ));
240                        path_bufs
241                            .entry( PathBuf::from( manifest_dir ).join( "Cargo.toml" ))
242                            .or_insert_with( || lines.map( ToOwned::to_owned ).collect() );
243    }}}}}
244    path_bufs
245}
246
247/// Allow the upstream crate to collect information from this crate.
248// The first line is manifest_dir
249// The rest lines are upstream package names, one per line.
250pub fn to( upstream: &str ) {
251    let out_path =
252        PathBuf::from(
253            env::var( "OUT_DIR" )
254                .expect( "$OUT_DIR should exist." )
255        ).join( MANIFEST_DIR_INWELLING );
256    if out_path.exists() {
257        let mut f = File::options().append( true ).open( &out_path )
258            .expect( &format!( "{:?} should be opened for appending.", out_path ));
259        writeln!( &mut f, "{}", upstream )
260            .expect( &format!( "An upstream name should be appended to {:?}.", out_path ));
261    } else {
262        let manifest_dir =
263            env::var( "CARGO_MANIFEST_DIR" )
264                .expect( "$CARGO_MANIFEST_DIR should exist." );
265        fs::write(
266            out_path,
267            format!( "{}\n{}\n", manifest_dir, upstream )
268        ).expect( "manifest_dir.txt generated." );
269    }
270}