/// Routinator UI build procedure /// /// This file is run when `cargo run` or `cargo build` is issued, by a user /// in this repo or in a dependent repo (most probably routinator itself). /// /// It will create a Rust file that contains all the necessary assets files /// (HTML, CSS, JS) for the routinator UI. That file will be exposed through /// an API that lives in libs.rs. extern crate reqwest; use flate2::read::GzDecoder; use reqwest::header; use std::env; use std::fs; use std::io; use std::io::prelude::*; use std::path::{Path, PathBuf}; use tar::Archive; // URL path and filename of assets file created by Github Actions. // See .github/workflows/release.yml in this repo. const DL_URL_PATH: &str = "https://github.com/NLnetLabs/routinator-ui/releases/download"; const DL_FILE_NAME: &str = "routinator-ui-build.tar.gz"; // Filename to use for saving the code generated by this build.rs const RS_FILE_NAME: &str = "ui-resources.rs"; // URL of files build locally by npm (in the parent directory of this build.rs). // - rebuild a new version of the UI by issuing `npm run build` in the parent dir, // (or another buid command, see package.json in the parent) // - remove the `dist` directory created by npm to have this build.rs use the // github release file instead. const SRC_DIR: &str = "../dist"; struct Asset { path: PathBuf, content: Vec, } struct Assets(Vec); impl Assets { fn new() -> Self { Assets(vec![]) } fn from_tar_gz(&mut self, tar_gz: Vec) -> io::Result<()> { let mut archive = Archive::new(GzDecoder::new(tar_gz.as_slice())); self.0 = archive .entries()? .map(move |e| { let content: &mut Vec = &mut vec![]; let mut e = e.ok()?; e.read_to_end(content).ok()?; if e.size() > 0 { Some(Asset { path: e.path().ok()?.to_path_buf(), content: content.to_owned(), }) } else { None } }) .filter_map(|e| e) .collect(); Ok(()) } fn from_files(&mut self, dir: std::fs::ReadDir) -> io::Result<()> { for e in dir { let entry = e?; let path = entry.path(); if path.is_dir() { self.from_files(path.read_dir()?)?; } else { let mut content_buf: Vec = vec![]; fs::File::open(entry.path())?.read_to_end(&mut content_buf)?; self.0.push(Asset { path: path .strip_prefix(SRC_DIR) .map_or_else(|e| Err(io::Error::new( io::ErrorKind::Other, format!( "routinator-ui: Path of Asset file {:?} does not start with /dist: {}", &path, e ) )), |p| Ok(p.to_path_buf()))?, content: content_buf, }); } } Ok(()) } fn write_to(self, dest_buf: std::cell::RefCell) -> io::Result<()> { dest_buf.borrow_mut().write_all( r#"mod ui_resources { pub fn endpoints_as_tuple() -> Vec<(&'static str, &'static [u8])> { vec!["# .as_bytes(), )?; for a in self.0 { add_asset_to_rs_file_from(a.path, &a.content, dest_buf.borrow_mut())?; } dest_buf.borrow_mut().write_all("]} }".as_bytes())?; Ok(()) } } fn _download_ui_release_build() -> Result, reqwest::Error> { let version = env!("CARGO_PKG_VERSION"); let mut headers = header::HeaderMap::new(); headers.insert( header::USER_AGENT, header::HeaderValue::from_str(&format!("User-Agent: routinator-ui/{}", version)) .expect("Cannot download routinator-ui-build."), ); let client = reqwest::blocking::Client::builder() .default_headers(headers) .build()?; let dl_url = format!("{}/v{}/{}", DL_URL_PATH, version, DL_FILE_NAME); let tar_gz_res = client.get(&dl_url).send()?; if !tar_gz_res.status().is_success() { eprintln!( "routinator-ui: Cannot continue building. The file {} is corrupt.", &dl_url ); std::process::exit(1); } Ok(tar_gz_res.bytes()?.to_vec()) } fn add_asset_to_rs_file_from( src_path: PathBuf, content_buf: &[u8], mut ui_buf: std::cell::RefMut, ) -> io::Result<()> { ui_buf.write_all(format!("(\"{}\",", src_path.to_string_lossy()).as_bytes())?; // To shorten the content_buf to a smaller slice size to avoid // building the complete file lengths (for debugging purposes) // uncomment the /*[..10]*/ part. ui_buf.write_all(format!("&{:?}", &content_buf /*[10]*/).as_bytes())?; ui_buf.write_all("),".as_bytes())?; Ok(()) } fn get_out_dir() -> Result { env::var_os("OUT_DIR") .ok_or_else(std::ffi::OsString::new)? .into_string() } fn main() { // build.rs gets rerun only if one of these conditions are met // - the cargo version in routinator-ui was bumped. // PLEASE DO NOT DO THIS MANUALLY, BUT USE `npm version patch|minor|major` INSTEAD. // See the README for more info. println!("cargo:rerun-if-env-changed=CARGO_PKG_VERSION"); // This build.rs file was changed. println!("cargo:rerun-if-changed=build.rs"); let rs_file_path: std::path::PathBuf; if let Ok(out_dir) = get_out_dir() { rs_file_path = Path::new(&out_dir).join(RS_FILE_NAME.to_string()); } else { panic!("in the streets of London."); }; // remove old rs file, if it exists. Will also catch read-only a file-system. if fs::metadata(&rs_file_path).is_ok() { if let Err(e) = fs::remove_file(&rs_file_path) { eprintln!( "routinator-ui: Cannot continue building. Failed to remove file {:?}: {}. Perhaps this is a read-only file system?", &rs_file_path, e ); std::process::exit(1); } }; // (re)create the rs file output file. let rs_file_buf = match fs::File::create(&rs_file_path) { Ok(f) => std::cell::RefCell::new(f), Err(e) => { eprintln!( "routinator-ui: Cannot continue building. Failed to create file {:?}: {}", &rs_file_path, e ); std::process::exit(1); } }; // If SRC_DIR exists (which means that a local build was made by Vue), // use that, otherwise download the assets file built by the release action on GitHub. let mut assets: Assets = Assets::new(); match fs::read_dir(Path::new(SRC_DIR)) { Ok(dir) => match assets.from_files(dir) { Ok(_) => {} Err(e) => { eprintln!( "routinator-ui: Cannot continue building. Failed to read local files from '/dist': {}", e ); std::process::exit(1); } }, Err(_) => match assets.from_tar_gz(_download_ui_release_build().unwrap()) { Ok(_) => {} Err(e) => { eprintln!( "routinator-ui: Cannot continue building. Failed to download release from github {:?}: {}", &rs_file_path, e ); std::process::exit(1); } }, } // flush the assets to disk in a .rs file match assets.write_to(rs_file_buf) { Ok(()) => {} Err(e) => { eprintln!( "routinator-ui: Cannot continue building. Failed to write to file {:?}: {}", &rs_file_path, e ); std::process::exit(1); } } }