1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
use std::env;
use std::ffi::OsStr;
use std::fs::create_dir_all;
use std::path::{Path, PathBuf};
use std::process::Command;

use cargo_metadata::MetadataCommand;
use walkdir::WalkDir;

fn manifest_dir() -> PathBuf {
    PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap())
        .canonicalize()
        .expect("Failed to canonicalize CARGO_MANIFEST_DIR")
}

fn out_dir() -> PathBuf {
    PathBuf::from(env::var("OUT_DIR").unwrap())
        .canonicalize()
        .expect("Failed to canonicalize OUT_DIR")
}

pub fn build() {
    // FlatBuffers
    if env::var("CARGO_FEATURE_FLATBUFFERS").ok().is_some() {
        build_flatbuffers();
    }

    // Proto (prost)
    if env::var("CARGO_FEATURE_PROTO").ok().is_some() {
        build_proto();
    }
}

pub fn build_proto() {
    let proto_dir = manifest_dir().join("proto");
    let proto_files = walk_files(&proto_dir, "proto");
    let proto_out = out_dir().join("proto");

    create_dir_all(&proto_out).expect("Failed to create proto output directory");

    // The proto include path contains all $CRATE/proto directories of ourself plus all of our
    // transitive dependencies.
    let metadata = MetadataCommand::new()
        .manifest_path(manifest_dir().join("Cargo.toml"))
        .exec()
        .unwrap();
    let proto_feature = "proto".to_string();
    let proto_includes = metadata
        .packages
        .iter()
        .filter(|&pkg| {
            pkg.features.contains_key(&proto_feature)
                || pkg.id == metadata.root_package().unwrap().id
        })
        .map(|pkg| pkg.manifest_path.parent().unwrap().join("proto"))
        .collect::<Vec<_>>();

    prost_build::Config::new()
        .out_dir(&proto_out)
        .compile_protos(&proto_files, proto_includes.as_slice())
        .expect("Failed to compile protos");
}

pub fn build_flatbuffers() {
    let flatbuffers_dir = manifest_dir().join("flatbuffers");
    let metadata = MetadataCommand::new()
        .manifest_path(manifest_dir().join("Cargo.toml"))
        .exec()
        .unwrap();
    let fbs_feature = "flatbuffers".to_string();
    let fbs_includes = metadata
        .packages
        .iter()
        .filter(|&pkg| {
            pkg.features.contains_key(&fbs_feature) || pkg.id == metadata.root_package().unwrap().id
        })
        .map(|pkg| {
            pkg.manifest_path
                .parent()
                .unwrap()
                .join("flatbuffers")
                .into_std_path_buf()
        })
        .collect::<Vec<_>>();

    let fbs_files = walk_files(&flatbuffers_dir, "fbs");

    let include_args: Vec<String> = fbs_includes
        .iter()
        .flat_map(|path| vec!["-I".to_string(), path.to_str().unwrap().to_string()])
        .collect();

    check_call(
        Command::new("flatc")
            .arg("--rust")
            .arg("--filename-suffix")
            .arg("")
            .args(include_args)
            .arg("--include-prefix")
            .arg("flatbuffers::deps")
            .arg("-o")
            .arg(out_dir().join("flatbuffers"))
            .args(fbs_files),
    )
}

/// Recursively walk for files with the given extension, adding them to rerun-if-changed.
fn walk_files(dir: &Path, ext: &str) -> Vec<PathBuf> {
    WalkDir::new(dir)
        .into_iter()
        .filter_map(|e| e.ok())
        .filter(|e| e.path().extension() == Some(OsStr::new(ext)))
        .map(|e| {
            rerun_if_changed(e.path());
            e.path().to_path_buf()
        })
        .collect()
}

fn rerun_if_changed(path: &Path) {
    println!(
        "cargo:rerun-if-changed={}",
        path.canonicalize()
            .unwrap_or_else(|_| panic!("failed to canonicalize {}", path.to_str().unwrap()))
            .to_str()
            .unwrap()
    );
}

fn check_call(command: &mut Command) {
    let name = command.get_program().to_str().unwrap().to_string();
    let Ok(status) = command.status() else {
        panic!("Failed to launch {}", &name)
    };
    if !status.success() {
        panic!("{} failed with status {}", &name, status.code().unwrap());
    }
}