re_web_server 0.2.0

Serves the Rerun web viewer (Wasm and HTML) over HTTP
Documentation
use cargo_metadata::{CargoOpt, Metadata, MetadataCommand, Package, PackageId};
use std::{
    collections::{HashMap, HashSet},
    fs,
    path::Path,
    process::Stdio,
};

// ---

// Mapping to cargo:rerun-if-changed with glob support
fn rerun_if_changed(path: &str) {
    // Workaround for windows verbatim paths not working with glob.
    // Issue: https://github.com/rust-lang/glob/issues/111
    // Fix: https://github.com/rust-lang/glob/pull/112
    // Fixed on upstream, but no release containing the fix as of writing.
    let path = path.trim_start_matches(r"\\?\");

    for path in glob::glob(path).unwrap() {
        println!("cargo:rerun-if-changed={}", path.unwrap().to_string_lossy());
    }
}

// ---

struct Packages<'a> {
    pkgs: HashMap<&'a str, &'a Package>,
}

impl<'a> Packages<'a> {
    pub fn from_metadata(metadata: &'a Metadata) -> Self {
        let pkgs = metadata
            .packages
            .iter()
            .map(|pkg| (pkg.name.as_str(), pkg))
            .collect::<HashMap<_, _>>();

        Self { pkgs }
    }

    /// Tracks an implicit dependency of the given name.
    ///
    /// This will generate all the appropriate `cargo:rerun-if-changed` clauses
    /// so that package `pkg_name` as well as all of it direct and indirect
    /// dependencies are properly tracked whether they are remote, in-workspace,
    /// or locally patched.
    pub fn track_implicit_dep(&self, pkg_name: &str) {
        let pkg = self.pkgs.get(pkg_name).unwrap_or_else(|| {
            let found_names: Vec<&str> = self.pkgs.values().map(|pkg| pkg.name.as_str()).collect();
            panic!("Failed to find package {pkg_name:?} among {found_names:?}")
        });

        // Track the root package itself
        {
            let mut path = pkg.manifest_path.clone();
            path.pop();

            // NOTE: Since we track the cargo manifest, past this point we only need to
            // account for locally patched dependencies.
            rerun_if_changed(path.join("Cargo.toml").as_ref());
            rerun_if_changed(path.join("**/*.rs").as_ref());
        }

        // Track all direct and indirect dependencies of that root package
        let mut tracked = HashSet::new();
        self.track_patched_deps(&mut tracked, pkg);
    }

    /// Recursively walk the tree of dependencies of the given `root` package, making sure
    /// to track all potentially modified, locally patched dependencies.
    fn track_patched_deps(&self, tracked: &mut HashSet<PackageId>, root: &Package) {
        for dep_pkg in root
            .dependencies
            .iter()
            // NOTE: We'd like to just use `dep.source`/`dep.path`, unfortunately they do not
            // account for crate patches at this level, so we build our own little index
            // and use that instead.
            .filter_map(|dep| self.pkgs.get(dep.name.as_str()))
        {
            let exists_on_local_disk = dep_pkg.source.is_none();
            if exists_on_local_disk {
                let mut dep_path = dep_pkg.manifest_path.clone();
                dep_path.pop();

                rerun_if_changed(dep_path.join("Cargo.toml").as_ref()); // manifest too!
                rerun_if_changed(dep_path.join("**/*.rs").as_ref());
            }

            if tracked.insert(dep_pkg.id.clone()) {
                self.track_patched_deps(tracked, dep_pkg);
            }
        }
    }
}

// Port of build_web.sh
fn build_web() {
    let repository_root_dir = format!("{}/../..", std::env!("CARGO_MANIFEST_DIR"));

    let crate_name = "re_viewer";
    let build_dir = format!("{repository_root_dir}/web_viewer");

    assert!(
        Path::new(&build_dir).exists(),
        "Failed to find dir {build_dir}. CWD: {:?}, CARGO_MANIFEST_DIR: {:?}",
        std::env::current_dir(),
        std::env!("CARGO_MANIFEST_DIR")
    );

    // Clean previous version of what we are building:
    let wasm_path = Path::new(&build_dir).join([crate_name, "_bg.wasm"].concat());
    fs::remove_file(wasm_path.clone()).ok();
    let js_path = Path::new(&build_dir).join([crate_name, ".js"].concat());
    fs::remove_file(js_path).ok();

    let metadata = MetadataCommand::new()
        .manifest_path("./Cargo.toml")
        .features(CargoOpt::AllFeatures)
        .exec()
        .unwrap();

    let target_wasm = format!("{}_wasm", metadata.target_directory);
    let release = std::env::var("PROFILE").unwrap() == "release";

    let root_dir = metadata.target_directory.parent().unwrap();

    // --------------------------------------------------------------------------------
    // Compile rust to wasm

    let mut cmd = std::process::Command::new("cargo");
    cmd.current_dir(root_dir);
    cmd.args([
        "build",
        "--target-dir",
        &target_wasm,
        "-p",
        crate_name,
        "--lib",
        "--target",
        "wasm32-unknown-unknown",
    ]);
    cmd.env("RUSTFLAGS", "--cfg=web_sys_unstable_apis");
    cmd.env("CARGO_ENCODED_RUSTFLAGS", "");

    if release {
        cmd.arg("--release");
    }

    eprintln!("wasm build cmd: {cmd:?}");

    let output = cmd
        .stdout(Stdio::inherit())
        .stderr(Stdio::inherit())
        .output()
        .expect("failed to compile re_viewer for wasm32");

    eprintln!("compile status: {}", output.status);
    eprintln!(
        "compile stderr: {}",
        String::from_utf8_lossy(&output.stderr)
    );

    assert!(output.status.success());

    // --------------------------------------------------------------------------------
    // Generate JS bindings

    let build = if release { "release" } else { "debug" };

    let target_name = [crate_name, ".wasm"].concat();

    let target_path = Path::new(&target_wasm)
        .join("wasm32-unknown-unknown")
        .join(build)
        .join(target_name);

    let mut cmd = std::process::Command::new("wasm-bindgen");
    cmd.current_dir(root_dir);
    cmd.args([
        target_path.to_str().unwrap(),
        "--out-dir",
        &build_dir,
        "--no-modules",
        "--no-typescript",
    ]);

    eprintln!("wasm-bindgen cmd: {cmd:?}");

    let output = cmd
        .stdout(Stdio::inherit())
        .stderr(Stdio::inherit())
        .output()
        .unwrap_or_else(|err| panic!("Failed to generate JS bindings: {err}. target_path: {target_path:?}, build_dir: {build_dir}"));

    eprintln!("wasm-bindgen status: {}", output.status);
    eprintln!(
        "wasm-bindgen stderr: {}",
        String::from_utf8_lossy(&output.stderr)
    );

    assert!(output.status.success());

    // --------------------------------------------------------------------------------
    // Optimize the wasm

    if release {
        let wasm_path = wasm_path.to_str().unwrap();

        // to get wasm-opt:  apt/brew/dnf install binaryen
        let mut cmd = std::process::Command::new("wasm-opt");
        cmd.current_dir(root_dir);
        cmd.args([wasm_path, "-O2", "--fast-math", "-o", wasm_path]);

        eprintln!("wasm-opt cmd: {cmd:?}");

        let output = cmd
            .stdout(Stdio::inherit())
            .stderr(Stdio::inherit())
            .output()
            .expect("failed to optimize wasm");

        eprintln!("wasm-opt status: {}", output.status);
        eprintln!(
            "wasm-opt stderr: {}",
            String::from_utf8_lossy(&output.stderr)
        );

        assert!(output.status.success());
    }
}

// ---

fn main() {
    if std::env::var("IS_IN_RERUN_WORKSPACE") != Ok("yes".to_owned()) {
        // Only run if we are in the rerun workspace, not on users machines.
        return;
    }
    if std::env::var("RERUN_IS_PUBLISHING") == Ok("yes".to_owned()) {
        // We don't need to rebuild - we should have done so beforehand!
        // See `RELEASES.md`
        return;
    }

    // Rebuild the web-viewer Wasm,
    // because the web_server library bundles it with `include_bytes!`

    let metadata = MetadataCommand::new()
        .features(CargoOpt::AllFeatures)
        .exec()
        .unwrap();

    rerun_if_changed("../../web_viewer/favicon.ico");
    rerun_if_changed("../../web_viewer/index.html");
    rerun_if_changed("../../web_viewer/sw.js");

    let pkgs = Packages::from_metadata(&metadata);
    // We implicitly depend on re_viewer, which means we also implicitly depend on
    // all of its direct and indirect dependencies (which are potentially in-workspace
    // or patched!).
    pkgs.track_implicit_dep("re_viewer");

    if std::env::var("CARGO_FEATURE___CI").is_ok() {
        // This saves a lot of CI time.
        eprintln!("__ci feature detected: Skipping building of web viewer wasm.");
    } else {
        eprintln!("Build web viewer wasm…");

        build_web();
    }
}