Skip to main content

embed_js_build/
lib.rs

1//! This crate provides functions to call from build and post-build scripts as part of
2//! wasm32-unknown-unknown builds that rely on crates using the `embed_js` crate to write inline
3//! javascript.
4//!
5//! See the `embed_js` repository for examples of how to use these crates together.
6
7extern crate embed_js_common;
8extern crate cpp_synmap;
9extern crate cpp_syn;
10extern crate serde_json;
11extern crate uuid;
12extern crate parity_wasm;
13
14use cpp_synmap::SourceMap;
15use cpp_syn::visit::Visitor;
16use cpp_syn::{Mac, TokenTree, Delimited};
17
18use parity_wasm::elements::{Module, Section, ExportEntry, Internal};
19
20use std::env;
21use std::path::{ PathBuf, Path };
22use std::io::{ BufWriter, BufReader, Read };
23use std::fs::File;
24use std::process::Command;
25use std::collections::hash_map::DefaultHasher;
26use std::hash::{Hash, Hasher};
27use std::collections::HashMap;
28
29use embed_js_common::{ JsMac, JsMacArg };
30
31struct JsVisitor<'a> {
32    source_map: &'a mut SourceMap,
33    instances: &'a mut Vec<JsMac>,
34    included_js: &'a mut String
35}
36impl<'a> Visitor for JsVisitor<'a> {
37    fn visit_mac(&mut self, mac: &Mac) {
38        if mac.path.segments.len() != 1 {
39            return;
40        }
41        let tts = match mac.tts[0] {
42            TokenTree::Delimited(Delimited { ref tts, .. }, _) => &**tts,
43            _ => return,
44        };
45        match mac.path.segments[0].ident.as_ref() {
46            "js" => {
47                if let Ok(parsed) = embed_js_common::parse_js_mac_source_map(tts, self.source_map) {
48                    self.instances.push(parsed);
49                }
50            }
51            "include_js" => {
52                let js_source = if let (Some(first), Some(last)) = (tts.first(), tts.last()) {
53                    self.source_map.source_text(first.span().extend(last.span())).unwrap()
54                } else {
55                    ""
56                };
57                self.included_js.push_str(&js_source);
58                self.included_js.push_str("\n");
59            }
60            "include" => {
61                use cpp_syn::{ Token, Lit, LitKind };
62                let mut iter = tts.iter().peekable();
63                match iter.next() {
64                    Some(&TokenTree::Token(Token::Literal(Lit { node: LitKind::Str(ref path, _), .. }), span)) => {
65                        if iter.next().is_some() {
66                            return;
67                        }
68                        let mut path = PathBuf::from(path);
69                        if !path.is_absolute() {
70                            let root = self.source_map.filename(span).unwrap();
71                            path = root.join(path);
72                        }
73                        println!("cargo:warning=embed_js_build processing source in included file {}", path.display());
74                        let krate = self.source_map.add_crate_root(path).unwrap();
75                        self.visit_crate(&krate);
76                    }
77                    Some(&TokenTree::Token(Token::Ident(ref ident), span)) if ident.as_ref() == "concat" => {
78                        match iter.next() {
79                            Some(&TokenTree::Token(Token::Not, _)) => {}
80                            _ => return
81                        }
82                        let tts = match iter.next() {
83                            Some(&TokenTree::Delimited(Delimited { ref tts, .. }, _)) => {
84                                tts
85                            }
86                            _ => return
87                        };
88                        let mut path = String::new();
89                        let mut iter = tts.iter().peekable();
90                        while let Some(t) = iter.next() {
91                            match *t {
92                                TokenTree::Token(Token::Literal(Lit { node: LitKind::Str(ref s, _), .. }), _) => {
93                                    path.push_str(s);
94                                }
95                                TokenTree::Token(Token::Comma, _) => {}
96                                TokenTree::Token(Token::Ident(ref ident), _) if ident.as_ref() == "env" => {
97                                    match iter.next() {
98                                        Some(&TokenTree::Token(Token::Not, _)) => {}
99                                        _ => return
100                                    }
101                                    let tts = match iter.next() {
102                                        Some(&TokenTree::Delimited(Delimited { ref tts, .. }, _)) => {
103                                            tts
104                                        }
105                                        _ => return
106                                    };
107                                    if let Some(&TokenTree::Token(Token::Literal(Lit { node: LitKind::Str(ref s, _), .. }), _)) = tts.first() {
108                                        if tts.len() != 1 {
109                                            return
110                                        }
111                                        if let Ok(v) = std::env::var(s) {
112                                            path.push_str(&v);
113                                        } else {
114                                            return
115                                        }
116                                    } else {
117                                        return
118                                    }
119                                }
120                                _ => return
121                            }
122                        }
123                        let mut path = PathBuf::from(path);
124                        if !path.is_absolute() {
125                            let root = self.source_map.filename(span).unwrap();
126                            path = root.join(path);
127                        }
128                        println!("cargo:warning=embed_js_build processing source in included file {}", path.display());
129                        let krate = self.source_map.add_crate_root(path).unwrap();
130                        self.visit_crate(&krate);
131                    }
132                    _ => return
133                }
134            }
135            _ => {}
136        }
137    }
138}
139
140/// Call this once from a build script for a crate that uses `embed_js` directly.
141///
142/// Parameters:
143///
144/// * `lib_root` The path to the crate root rust file, e.g. "src/lib.rs"
145///
146/// Example:
147///
148/// ```ignore
149/// extern crate embed_js_build;
150/// fn main() {
151///     use std::path::PathBuf;
152///     let root = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap()).join("src/lib.rs");
153///     embed_js_build::preprocess_crate(&root);
154/// }
155/// ```
156pub fn preprocess_crate(lib_root: &Path) {
157    let mut source_map = SourceMap::new();
158    let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap());
159    let mut instances = Vec::new();
160    let mut included_js = String::new();
161    let krate = source_map.add_crate_root(lib_root).unwrap();
162    JsVisitor {
163        source_map: &mut source_map,
164        instances: &mut instances,
165        included_js: &mut included_js
166    }.visit_crate(&krate);
167
168    let js_path = out_dir.join("embed_js_data.json");
169    serde_json::to_writer(BufWriter::new(File::create(&js_path).unwrap()), &(instances, included_js)).unwrap();
170    let preamble_path = out_dir.join("embed_js_preamble.rs");
171    File::create(preamble_path).unwrap();
172}
173
174/// Generated from `postprocess_crate`.
175pub struct PostProcessData {
176    /// The path to the generated wasm binary.
177    pub wasm_path: PathBuf,
178    /// The contents of the wasm binary, provided for convenience.
179    pub wasm: Vec<u8>,
180    /// The javascript that should be put as the value of the `env` field in the `importObject`
181    /// passed to `WebAssembly.instantiate`.
182    pub imports: String,
183    /// All javascript specified by the `include_js` macro in linked crates. This should be run
184    /// before the WebAssembly module is loaded.
185    pub included: String
186}
187/// Call this once **after** a wasm-unknown-unknown build has completed (i.e. from a post-build
188/// script) in order to generate the javascript imports that should accompany the wasm binary.
189///
190/// See the `embed_js` repository for example projects using this function.
191///
192/// Parameters:
193///
194/// * `lib_name` The binary name to process, typically the name of the crate unless set otherwise
195///   in `Cargo.toml`.
196/// * `debug` Whether to look for the debug or release binary to process. Until wasm32-unkown-unknown
197///   supports debug builds, this should always be set to `false`.
198///
199/// Example post-build script, taken from the "simple" example in the `embed_js` repository:
200///
201/// ```ignore
202/// extern crate base64;
203/// extern crate embed_js_build;
204///
205/// use std::fs::File;
206/// use std::io::Write;
207///
208/// fn main() {
209///     let pp_data = embed_js_build::postprocess_crate("simple", false).unwrap();
210///     let in_base_64 = base64::encode(&pp_data.wasm);
211///     let html_path = pp_data.wasm_path.with_extension("html");
212///     let mut html_file = File::create(&html_path).unwrap();
213///     write!(html_file, r#"<!DOCTYPE html>
214/// <html lang="en">
215/// <head>
216/// <meta charset="utf-8">
217/// <title> wasm test </title>
218/// <script>
219/// function _base64ToArrayBuffer(base64) {{
220///     var binary_string =  window.atob(base64);
221///     var len = binary_string.length;
222///     var bytes = new Uint8Array( len );
223///     for (var i = 0; i < len; ++i) {{
224///         bytes[i] = binary_string.charCodeAt(i);
225///     }}
226///     return bytes.buffer;
227/// }}
228/// var bytes = _base64ToArrayBuffer(
229/// "{}"
230/// );
231/// WebAssembly.instantiate(bytes, {{ env: {{
232/// {}
233/// }}}}).then(results => {{
234///     window.exports = results.instance.exports;
235///     console.log(results.instance.exports.add_two(2));
236/// }});
237/// </script>
238/// </head>
239/// </html>
240/// "#,
241///            in_base_64,
242///            pp_data.imports
243///     ).unwrap();
244/// }
245pub fn postprocess_crate(lib_name: &str, debug: bool) -> std::io::Result<PostProcessData> {
246    let metadata_json = Command::new("cargo").args(&["metadata", "--format-version", "1"]).output().unwrap().stdout;
247    let metadata_json: serde_json::Value = serde_json::from_slice(&metadata_json).unwrap();
248    let target_directory = Path::new(metadata_json.as_object().unwrap().get("target_directory").unwrap().as_str().unwrap());
249    let bin_prefix = target_directory.join(&format!("wasm32-unknown-unknown/{}/{}", if debug { "debug" } else { "release" }, lib_name));
250
251    // collect json data from all dependency crates
252    let d_path = bin_prefix.with_extension("d");
253    let mut d_string = String::new();
254    File::open(&d_path)?.read_to_string(&mut d_string).unwrap();
255    let mut d_pieces: Vec<String> = d_string.split_whitespace().map(String::from).collect::<Vec<_>>();
256    { // stick escaped spaces back together
257        let mut i = 0;
258        while i < d_pieces.len() {
259            while d_pieces[i].ends_with("\\") && i != d_pieces.len() - 1 {
260                let removed = d_pieces.remove(i+1);
261                d_pieces[i].push_str(&removed);
262            }
263            i += 1;
264        }
265    }
266    d_pieces.remove(0); // remove lib path
267    let mut js_macs: HashMap<String, JsMac> = HashMap::new();
268    let mut included_js = String::new();
269    for path in d_pieces {
270        if path.ends_with("out/embed_js_preamble.rs") || path.ends_with("out\\embed_js_preamble.rs") {
271            let data_path = PathBuf::from(path).with_file_name("embed_js_data.json");
272            let (mut crate_js_macs, crate_included_js): (Vec<JsMac>, String) = serde_json::from_reader(BufReader::new(File::open(data_path)?)).unwrap();
273            included_js.push_str(&crate_included_js);
274            for js_mac in crate_js_macs.drain(..) {
275                let mut hasher = DefaultHasher::new();
276                js_mac.hash(&mut hasher);
277                let mac_hash = hasher.finish();
278                let key = format!("__embed_js__{:x}", mac_hash);
279                if let Some(existing) = js_macs.get(&key) {
280                    if *existing != js_mac {
281                        panic!("A hash collision has occurred in the embed_js build process. Please raise a bug! Meanwhile, try making small changes to your embedded js to remove the collision.")
282                    }
283                }
284                js_macs.insert(key, js_mac);
285            }
286        }
287    }
288
289    let wasm_path = bin_prefix.with_extension("wasm");
290    match Command::new("wasm-gc").args(&[&wasm_path, &wasm_path]).output() {
291        Ok(output) => {
292            if !output.status.success() {
293                panic!("wasm-gc encountered an error.\n\nstatus: {}\n\nstdout:\n\n{}\n\nstderr:\n\n{}",
294                       output.status,
295                       String::from_utf8(output.stdout).unwrap_or_else(|_| String::from("<error decoding stdout>")),
296                       String::from_utf8(output.stderr).unwrap_or_else(|_| String::from("<error decoding stderr>")))
297            }
298        }
299        Err(e) => panic!("Error attempting to run wasm-gc. Have you got it installed? Error message: {}", e)
300    }
301    let mut wasm = Vec::new();
302    BufReader::new(File::open(&wasm_path)?).read_to_end(&mut wasm)?;
303    let mut module: Module = parity_wasm::deserialize_buffer(wasm.clone()).unwrap();
304    // modify the module to export the function table
305    let has_table_export = module.export_section()
306        .map(|exports| exports.entries()
307            .iter()
308            .any(|entry| entry.field() == "__table"))
309        .unwrap_or(false);
310    if !has_table_export && module.table_section().is_some() {
311        let sections = module.sections_mut();
312        for section in sections {
313            match *section {
314                Section::Export(ref mut exports) => {
315                    exports.entries_mut().push(ExportEntry::new("__table".to_string(), Internal::Table(0)));
316                    break;
317                }
318                _ => {}
319            }
320        }
321    }
322    parity_wasm::serialize_to_file(&wasm_path, module.clone()).unwrap();
323    wasm.clear();
324    BufReader::new(File::open(&wasm_path)?).read_to_end(&mut wasm)?;
325    let mut imports = String::new();
326    if let Some(import_section) = module.import_section() {
327        for entry in import_section.entries() {
328            if entry.module() == "env" {
329                if let Some(mac) = js_macs.remove(entry.field()) {
330                    if !imports.is_empty() {
331                        imports.push_str(",\n");
332                    }
333                    imports.push_str(&format!("{}:function(", entry.field()));
334                    let mut start = true;
335                    for arg in mac.args {
336                        if !start {
337                            imports.push_str(", ");
338                        } else {
339                            start = false;
340                        }
341                        match arg {
342                            JsMacArg::Ref(_, _, name) |
343                            JsMacArg::Primitive(_, name, _) => imports.push_str(&name)
344                        }
345                    }
346                    if let Some(body) = mac.body {
347                        imports.push_str(&format!("){{{}}}", body));
348                    } else {
349                        imports.push_str("){}\n");
350                    }
351                }
352            }
353        }
354    }
355
356    // find
357    Ok(PostProcessData {
358        wasm_path,
359        wasm,
360        included: included_js,
361        imports
362    })
363}