Skip to main content

arfur_build/
runner.rs

1use std::{fs, io::Cursor, os::unix::fs::PermissionsExt, path::Path};
2
3use crate::library::Library;
4
5use color_eyre::{Help, Result};
6
7/// The main build script runner. See [`Self::run`] for more details.
8pub struct Runner<'a, T: Library> {
9    /// The desired WPILib version.
10    version: &'a str,
11
12    /// The desired NI libraries' version.
13    ni_version: &'a str,
14
15    /// A list of libraries it should install.
16    libraries: Vec<T>,
17
18    /// The contents of the header file that bindgen will build.
19    header_contents: &'a str,
20
21    /// The allowlist that will be passed to bindgen.
22    allowlist: &'a str,
23
24    /// A list of library names we should link to.
25    lib_list: &'a [&'a str],
26
27    /// The output directory. Among other things, runner will output the .rs
28    /// bindings here.
29    output_directory: &'a Path,
30
31    /// Additional arguments passed to clang during bindgen.
32    clang_args: String,
33}
34
35impl<'a, T: Library> Runner<'a, T> {
36    /// Create a new [`Runner`].
37    pub fn new(
38        version: &'a str,
39        ni_version: &'a str,
40        libraries: Vec<T>,
41        header_contents: &'a str,
42        allowlist: &'a str,
43        lib_list: &'a [&'a str],
44        output_directory: &'a Path,
45        clang_args: String,
46    ) -> Self {
47        Self {
48            version,
49            ni_version,
50            libraries,
51            header_contents,
52            allowlist,
53            lib_list,
54            output_directory,
55            clang_args,
56        }
57    }
58
59    /// Run the build script.
60    pub async fn run(&mut self, link_only: bool) -> Result<()> {
61        let complete_marker_path = self.output_directory.join("arfur.complete");
62
63        if !complete_marker_path.exists() && !link_only {
64            self.download_libraries()
65                .await
66                .note("Failed to download libraries.")?;
67
68            self.install_libraries()
69                .await
70                .note("Failed to install libraries.")?;
71
72            #[cfg(feature = "bindgen")]
73            self.generate_bindings()
74                .await
75                .note("Failed to generate bindings.")?;
76
77            self.cleanup().note("Failed to clean up after build.")?;
78        } else {
79            println!("Built copy found, not building again...");
80        }
81
82        self.link_libraries()
83            .note("Failed to ask Cargo to link to libraries.")?;
84
85        Ok(())
86    }
87
88    /// Download the libraries from the FRC Maven JFrog repository. This method
89    /// downloads to {output_directory}/raw/. If this function succeeds, every
90    /// FRC-related library should be available (unzipped) in this directory.
91    pub async fn download_libraries(&mut self) -> Result<()> {
92        let extracted_dir = self.output_directory.join("raw");
93
94        for library in &self.libraries {
95            let link = library.get_link(self.version, self.ni_version);
96
97            let zipped = reqwest::get(link)
98                .await
99                .note("Failed to download archive.")?
100                .bytes()
101                .await
102                .note("Failed to convert archive into bytes.")?;
103
104            zip_extract::extract(Cursor::new(zipped), &extracted_dir, false)
105                .note("Failed to extract zip file.")?;
106        }
107
108        Ok(())
109    }
110
111    /// Ready the libraries for linking. While in theory this step should also
112    /// move them to a separate directory, it's much easier to keep them in
113    /// their existing directory and make changes on that.
114    ///
115    /// This method will set all .so files to executable, remove the .debug
116    /// files, and rename malformed files (e.g. libX.so.22.0.0 to libX.so).
117    pub async fn install_libraries(&mut self) -> Result<()> {
118        let dynamic_library_dir = self
119            .output_directory
120            .join("raw")
121            .join("linux")
122            .join("athena")
123            .join("shared");
124
125        fs::set_permissions(&dynamic_library_dir, fs::Permissions::from_mode(0o755))?;
126        for file in fs::read_dir(&dynamic_library_dir)? {
127            let file = file?;
128            fs::set_permissions(&file.path(), fs::Permissions::from_mode(0o755))?;
129
130            if file.file_name().to_str().unwrap().ends_with(".debug") {
131                // If it's a debug file, just delete it.
132                fs::remove_file(file.path())?;
133            } else if !&file.file_name().to_str().unwrap().ends_with(".so") {
134                // The file does not end with .so, so rename it by popping the
135                // last 7 characters.
136                //
137                // Turns `libX.so.22.0.0` to `libX.so`.
138
139                let name = file.file_name();
140                let mut name = name.to_str().unwrap().chars();
141                for _ in 1..8 {
142                    name.next_back();
143                }
144                let name = name.as_str();
145                let mut new_name = file.path();
146                new_name.set_file_name(name);
147                fs::rename(file.path(), new_name)?;
148            }
149        }
150
151        Ok(())
152    }
153
154    /// Ask Cargo to link to the dynamic libraries.
155    pub fn link_libraries(&mut self) -> Result<()> {
156        let dynamic_library_dir = self
157            .output_directory
158            .join("raw")
159            .join("linux")
160            .join("athena")
161            .join("shared");
162
163        for lib in self.lib_list.iter() {
164            println!("cargo:rustc-link-lib=dylib={}", lib);
165        }
166
167        println!(
168            "cargo:rustc-link-search=native={dynamic_library_dir}",
169            dynamic_library_dir = dynamic_library_dir.to_str().unwrap()
170        );
171
172        Ok(())
173    }
174
175    /// Use `bindgen` to generate bindings.
176    #[cfg(feature = "bindgen")]
177    pub async fn generate_bindings(&mut self) -> Result<()> {
178        let raw_directory = self.output_directory.join("raw");
179
180        let bindings = bindgen::Builder::default()
181            // TODO: check if this works with more than one runner.
182            .header_contents("runner-header", self.header_contents)
183            .parse_callbacks(Box::new(bindgen::CargoCallbacks))
184            .enable_cxx_namespaces()
185            .allowlist_function(self.allowlist)
186            .allowlist_type(self.allowlist)
187            .allowlist_var(self.allowlist)
188            .clang_arg(format!("-I{}", raw_directory.to_str().unwrap()))
189            .clang_arg(self.clang_args.clone())
190            .clang_arg("-std=c++17")
191            .clang_args(&["-x", "c++"]);
192
193        println!("clang command: {:?}", bindings.command_line_flags());
194
195        bindings
196            .generate()
197            .note("Failed to generate bindings...")?
198            .write_to_file(self.output_directory.join("bindings.rs"))
199            .note("Failed to write bindings file...")?;
200
201        Ok(())
202    }
203
204    /// Clean up after a run.
205    pub fn cleanup(&mut self) -> Result<()> {
206        let complete_marker_path = self.output_directory.join("arfur.complete");
207
208        fs::File::create(complete_marker_path)?;
209
210        Ok(())
211    }
212}