Skip to content

Commit

Permalink
bundle UI files into the binary
Browse files Browse the repository at this point in the history
This is optional but now enabled for release builds.

Why?

* It shrinks the release docker images a bit, as the binary
  includes only the gzipped version of files and uncompressed into RAM
  at startup (which should be fast).

* It's a step toward #160.
  • Loading branch information
scottlamb committed Aug 6, 2023
1 parent 02ac1a5 commit faba358
Show file tree
Hide file tree
Showing 13 changed files with 534 additions and 61 deletions.
10 changes: 9 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,15 +36,23 @@ jobs:
# The retry here is to work around "Unable to connect to azure.archive.ubuntu.com" errors.
# https://github.com/actions/runner-images/issues/6894
run: sudo apt-get --option=APT::Acquire::Retries=3 update && sudo apt-get --option=APT::Acquire::Retries=3 install libncurses-dev libsqlite3-dev pkgconf
- uses: actions/setup-node@v2
with:
node-version: 18
- name: Install Rust
uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: ${{ matrix.rust }}
override: true
components: ${{ matrix.extra_components }}
- run: cd ui && npm ci
- run: cd ui && npm run build
- name: Test
run: cd server && cargo test ${{ matrix.extra_args }} --all
run: |
cd server
cargo test ${{ matrix.extra_args }} --all
cargo test --features=bundled-ui ${{ matrix.extra_args }} --all
continue-on-error: ${{ matrix.rust == 'nightly' }}
- name: Check formatting
if: matrix.rust == 'stable'
Expand Down
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ even on minor releases, e.g. `0.7.5` -> `0.7.6`.
* fix [#289](https://github.com/scottlamb/moonfire-nvr/issues/289): crash on
pressing the `Add` button in the sample file directory dialog
* log to `stderr` again, fixing a regression with the `tracing` change in 0.7.6.

* experimental (off by default) support for bundling UI files into the
executable.

## 0.7.6 (2023-07-08)

Expand Down
2 changes: 1 addition & 1 deletion docker/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ FROM --platform=$BUILDPLATFORM dev AS build-server
LABEL maintainer="[email protected]"
ARG INVALIDATE_CACHE_BUILD_SERVER=
COPY docker/build-server.bash /
COPY --from=build-ui /var/lib/moonfire-nvr/src/ui/build /var/lib/moonfire-nvr/src/ui/build
RUN --mount=type=cache,id=target,target=/var/lib/moonfire-nvr/target,sharing=locked,mode=777 \
--mount=type=cache,id=cargo,target=/cargo-cache,sharing=locked,mode=777 \
--mount=type=bind,source=server,target=/var/lib/moonfire-nvr/src/server,readonly \
Expand All @@ -66,7 +67,6 @@ COPY --from=dev /docker-build-debug/dev/ /docker-build-debug/dev/
COPY --from=build-server /docker-build-debug/build-server/ /docker-build-debug/build-server/
COPY --from=build-server /usr/local/bin/moonfire-nvr /usr/local/bin/moonfire-nvr
COPY --from=build-ui /docker-build-debug/build-ui /docker-build-debug/build-ui
COPY --from=build-ui /var/lib/moonfire-nvr/src/ui/build /usr/local/lib/moonfire-nvr/ui

# The install instructions say to use --user in the docker run commandline.
# Specify a non-root user just in case someone forgets.
Expand Down
4 changes: 2 additions & 2 deletions docker/build-server.bash
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ ln -s /cargo-cache/{git,registry} ~/.cargo

build_profile=release-lto
cd src/server
time cargo test
time cargo build --profile=$build_profile
time cargo test --features=bundled-ui
time cargo build --features=bundled-ui --profile=$build_profile
find /cargo-cache -ls > /docker-build-debug/build-server/cargo-cache-after
find ~/target -ls > /docker-build-debug/build-server/target-after
sudo install -m 755 \
Expand Down
30 changes: 30 additions & 0 deletions server/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 11 additions & 4 deletions server/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,15 @@ rust-version = "1.70"
publish = false

[features]

# The nightly feature is used within moonfire-nvr itself to gate the
# benchmarks. Also pass it along to crates that can benefit from it.
nightly = ["db/nightly"]

# The bundled feature includes bundled (aka statically linked) versions of
# native libraries where possible.
bundled = ["rusqlite/bundled"]
# The bundled feature aims to make a single executable file that is deployable,
# including statically linked libraries and embedded UI files.
bundled = ["rusqlite/bundled", "bundled-ui"]

bundled-ui = []

[workspace]
members = ["base", "db"]
Expand Down Expand Up @@ -71,6 +72,12 @@ tracing-log = "0.1.3"
ulid = "1.0.0"
url = "2.1.1"
uuid = { version = "1.1.2", features = ["serde", "std", "v4"] }
flate2 = "1.0.26"

[build-dependencies]
blake3 = "1.0.0"
fnv = "1.0"
walkdir = "2.3.3"

[dev-dependencies]
mp4 = { git = "https://github.com/scottlamb/mp4-rust", branch = "moonfire" }
Expand Down
149 changes: 149 additions & 0 deletions server/build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
// This file is part of Moonfire NVR, a security camera network video recorder.
// Copyright (C) 2023 The Moonfire NVR Authors; see AUTHORS and LICENSE.txt.
// SPDX-License-Identifier: GPL-v3.0-or-later WITH GPL-3.0-linking-exception

//! Build script to bundle UI files if `bundled-ui` Cargo feature is selected.

use std::fmt::Write;
use std::path::{Path, PathBuf};
use std::process::ExitCode;

const UI_DIR: &str = "../ui/build";

fn ensure_link(original: &Path, link: &Path) {
match std::fs::read_link(link) {
Ok(dst) if dst == original => return,
Err(e) if e.kind() != std::io::ErrorKind::NotFound => {
panic!("couldn't create link {link:?} to original path {original:?}: {e}")
}
_ => {}
}
std::os::unix::fs::symlink(original, link).expect("symlink creation should succeed");
}

struct File {
/// Path with `ui_files/` prefix and the encoding suffix; suitable for
/// passing to `include_bytes!` in the expanded code.
///
/// E.g. `ui_files/index.html.gz`.
include_path: String,
encoding: FileEncoding,
etag: blake3::Hash,
}

#[derive(Copy, Clone)]
enum FileEncoding {
Uncompressed,
Gzipped,
}

impl FileEncoding {
fn to_str(self) -> &'static str {
match self {
Self::Uncompressed => "FileEncoding::Uncompressed",
Self::Gzipped => "FileEncoding::Gzipped",
}
}
}

/// Map of "bare path" to the best representation.
///
/// A "bare path" has no prefix for the root and no suffix for encoding, e.g.
/// `favicons/blah.ico` rather than `../../ui/build/favicons/blah.ico.gz`.
///
/// The best representation is gzipped if available, uncompressed otherwise.
type FileMap = fnv::FnvHashMap<String, File>;

fn stringify_files(files: &FileMap) -> Result<String, std::fmt::Error> {
let mut buf = String::new();
write!(buf, "const FILES: [BuildFile; {}] = [\n", files.len())?;
for (bare_path, file) in files {
let include_path = &file.include_path;
let etag = file.etag.to_hex();
let encoding = file.encoding.to_str();
write!(buf, " BuildFile {{ bare_path: {bare_path:?}, data: include_bytes!({include_path:?}), etag: {etag:?}, encoding: {encoding} }},\n")?;
}
write!(buf, "];\n")?;
Ok(buf)
}

fn main() -> ExitCode {
// Explicitly declare dependencies, so this doesn't re-run if other source files change.
println!("cargo:rerun-if-changed=build.rs");

// Nothing to do if the feature is off. cargo will re-run if features change.
if !cfg!(feature = "bundled-ui") {
return ExitCode::SUCCESS;
}

// If the feature is on, also re-run if the actual UI files change.
println!("cargo:rerun-if-changed={UI_DIR}");

let out_dir: PathBuf = std::env::var_os("OUT_DIR")
.expect("cargo should set OUT_DIR")
.into();

let abs_ui_dir = std::fs::canonicalize(UI_DIR)
.expect("ui dir should be accessible. Did you run `npm run build` first?");

let mut files = FileMap::default();
for entry in walkdir::WalkDir::new(&abs_ui_dir) {
let entry = match entry {
Ok(e) => e,
Err(e) => {
eprintln!(
"walkdir failed. Did you run `npm run build` first?\n\n\
caused by:\n{e}"
);
return ExitCode::FAILURE;
}
};
if !entry.file_type().is_file() {
continue;
}

let path = entry
.path()
.strip_prefix(&abs_ui_dir)
.expect("walkdir should return root-prefixed entries");
let path = path.to_str().expect("ui file paths should be valid UTF-8");
let (bare_path, encoding);
match path.strip_suffix(".gz") {
Some(p) => {
bare_path = p;
encoding = FileEncoding::Gzipped;
}
None => {
bare_path = path;
encoding = FileEncoding::Uncompressed;
if files.get(bare_path).is_some() {
continue; // don't replace with suboptimal encoding.
}
}
}

let contents = std::fs::read(entry.path()).expect("ui files should be readable");
let etag = blake3::hash(&contents);
let include_path = format!("ui_files/{path}");
files.insert(
bare_path.to_owned(),
File {
include_path,
encoding,
etag,
},
);
}

let files = stringify_files(&files).expect("write to String should succeed");
let mut out_rs_path = std::path::PathBuf::new();
out_rs_path.push(&out_dir);
out_rs_path.push("ui_files.rs");
std::fs::write(&out_rs_path, files).expect("writing ui_files.rs should succeed");

let mut out_link_path = std::path::PathBuf::new();
out_link_path.push(&out_dir);
out_link_path.push("ui_files");
ensure_link(&abs_ui_dir, &out_link_path);
return ExitCode::SUCCESS;
}
Loading

0 comments on commit faba358

Please sign in to comment.