Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ portpicker = "=0.1.1"

[build-dependencies]
tonic-prost-build = "0.14.1"
walkdir.workspace = true

[profile.release]
lto = true
Expand Down
125 changes: 102 additions & 23 deletions build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@

//! A build script to generate the gRPC OTLP receiver API (client and server stubs.

use std::process::Command;
use std::{
ffi::OsStr,
path::Path,
process::{Command, ExitStatus},
time::SystemTime,
};

fn main() -> Result<(), Box<dyn std::error::Error>> {
// The gRPC OTLP Receiver is vendored in `src/otlp_receiver/receiver` to avoid
Expand Down Expand Up @@ -31,45 +36,76 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
Ok(())
}

fn timestamp(dir: walkdir::DirEntry) -> Result<SystemTime, Box<dyn std::error::Error>> {
let md = dir.metadata()?;
Ok(md.modified()?)
}

/// Helper function to determine if NPM project is out of date.
fn is_ui_stale(dir: &Path) -> Result<bool, Box<dyn std::error::Error>> {
// If any output directories don't exist, rebuild.
if !dir.join("dist").exists() {
return Ok(true);
}
if !dir.join("node_modules").exists() {
return Ok(true);
}
// If our lock file is out of date with our package file, we need to rebuild.
let lock_timestamp = dir.join("package-lock.json").metadata()?.modified()?;
let package_timestamp = dir.join("package.json").metadata()?.modified()?;
if package_timestamp > lock_timestamp {
return Ok(true);
}
// Now check source files. This may be a bit expensive, as we continuously check last modified.
let last_build = dir.join("dist/index.html").metadata()?.modified()?;
for entry in walkdir::WalkDir::new(dir.join("src")) {
if timestamp(entry?)? > last_build {
return Ok(true);
}
}
Ok(false)
}

fn build_ui() -> Result<(), Box<dyn std::error::Error>> {
let ui_dir = std::path::Path::new("ui");
let ui_dir = Path::new("ui");

// Check if UI is out of date before running.
if !is_ui_stale(ui_dir)? {
return Ok(());
}

// Get the npm command - on Windows it's npm.cmd, on Unix it's npm
let npm_cmd = if cfg!(target_os = "windows") {
"npm.cmd"
let mut npm_runner = if cfg!(target_os = "windows") {
NpmRunner::NpmExec("npm.cmd".to_owned())
} else {
"npm"
NpmRunner::NpmExec("npm".to_owned())
};

// Check if npm is available
let npm_check = Command::new(npm_cmd).arg("--version").output();

if npm_check.is_err() {
return Err(
"npm not found. Please install Node.js and npm from https://nodejs.org/ to build this project."
.into(),
);
if !npm_runner.check_valid() {
println!("cargo:warning=npm not found. Please install Node.js and npm from https://nodejs.org/ to build this project.");
println!("cargo:warning=Attempting to use docker for now.");
// TODO - Docker usage should ALWAYS be behind some kind of flag, ideally.
npm_runner = NpmRunner::Docker;
}

// Check if npm is available
println!("cargo:warning=Building UI...");

// Always update dependencies to exactly match latest package-lock.json
println!("cargo:warning=Checking UI dependencies...");
let status = Command::new(npm_cmd)
.arg("ci")
.current_dir(ui_dir)
.status()?;
let status = npm_runner.run(ui_dir, vec!["ci"])?;

if !status.success() {
return Err("Failed to install UI dependencies".into());
println!("cargo:warning=Unable to use installed npm, using docker instead...");
npm_runner = NpmRunner::Docker;
let status = npm_runner.run(ui_dir, vec!["ci"])?;
if !status.success() {
return Err("Failed to load UI dependencies".into());
}
}

// Build the UI
let status = Command::new(npm_cmd)
.arg("run")
.arg("build")
.current_dir(ui_dir)
.status()?;
let status = npm_runner.run(ui_dir, vec!["run", "build"])?;

if !status.success() {
return Err("Failed to build UI".into());
Expand All @@ -79,3 +115,46 @@ fn build_ui() -> Result<(), Box<dyn std::error::Error>> {

Ok(())
}

enum NpmRunner {
NpmExec(String),
Docker,
}

impl NpmRunner {
// Returns true if this is a valid way to run NPM.
fn check_valid(&self) -> bool {
match self {
NpmRunner::NpmExec(npm_cmd) => Command::new(npm_cmd).arg("--version").output().is_ok(),
// TODO - figure out how to test docker install.
NpmRunner::Docker => true,
}
}

/// Runs the given NPM command given a chosen runner.
fn run<I, S>(&self, dir: &Path, cmd: I) -> Result<ExitStatus, Box<dyn std::error::Error>>
where
I: IntoIterator<Item = S>,
S: AsRef<OsStr>,
{
let result = match self {
NpmRunner::NpmExec(npm) => Command::new(npm).args(cmd).current_dir(dir).status()?,
NpmRunner::Docker => {
Command::new("docker")
.arg("run")
.arg("--rm")
.arg("-v")
.arg(".:/app")
.arg("-w")
.arg("/app")
// TODO - This version should get pulled from somewhere.
.arg("node:lts-alpine")
.arg("npm")
.args(cmd)
.current_dir(dir)
.status()?
}
};
Ok(result)
}
}
1 change: 1 addition & 0 deletions ui/.npmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
engine-strict=true
4 changes: 4 additions & 0 deletions ui/package-lock.json

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

4 changes: 4 additions & 0 deletions ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
"private": true,
"version": "0.0.1",
"type": "module",
"engines": {
"node": ">=24 <25",
"npm": ">=11 <12"
},
"scripts": {
"dev": "vite",
"build": "vite build",
Expand Down