trailbase_build/
lib.rs

1#![allow(clippy::needless_return)]
2
3use log::*;
4use std::fs::{self};
5use std::io::{Result, Write};
6use std::path::{Path, PathBuf};
7
8pub fn build_protos(proto_path: impl AsRef<Path>) -> Result<()> {
9  let path = proto_path.as_ref().to_string_lossy();
10  println!("cargo::rerun-if-changed={path}");
11
12  let prost_config = {
13    let mut config = prost_build::Config::new();
14    config.enum_attribute(".", "#[derive(serde::Serialize, serde::Deserialize)]");
15    config
16  };
17
18  let proto_files = vec![
19    PathBuf::from(format!("{path}/config.proto")),
20    PathBuf::from(format!("{path}/config_api.proto")),
21    PathBuf::from(format!("{path}/vault.proto")),
22  ];
23
24  prost_reflect_build::Builder::new()
25    .descriptor_pool("crate::DESCRIPTOR_POOL")
26    .compile_protos_with_config(prost_config, &proto_files, &[proto_path])?;
27
28  return Ok(());
29}
30
31pub fn copy_dir(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> Result<()> {
32  fs::create_dir_all(&dst)?;
33  for entry in fs::read_dir(src)? {
34    let entry = entry?;
35    if entry.file_name().to_string_lossy().starts_with(".") {
36      continue;
37    }
38
39    if entry.file_type()?.is_dir() {
40      copy_dir(entry.path(), dst.as_ref().join(entry.file_name()))?;
41    } else {
42      fs::copy(entry.path(), dst.as_ref().join(entry.file_name()))?;
43    }
44  }
45
46  return Ok(());
47}
48
49fn write_output(mut sink: impl Write, source: &[u8], header: &str) -> Result<()> {
50  sink.write_all(header.as_bytes())?;
51  sink.write_all(b"\n")?;
52  sink.write_all(source)?;
53  sink.write_all(b"\n\n")?;
54  return Ok(());
55}
56
57pub fn pnpm_run(args: &[&str]) -> Result<std::process::Output> {
58  let cmd = "pnpm";
59  let output = std::process::Command::new(cmd)
60    .args(args)
61    .output()
62    .map_err(|err| {
63      eprintln!("Error: Failed to run '{cmd} {}'", args.join(" "));
64      return err;
65    })?;
66
67  let header = format!(
68    "== {cmd} {} (cwd: {:?}) ==",
69    args.join(" "),
70    std::env::current_dir()?
71  );
72  write_output(std::io::stdout(), &output.stdout, &header)?;
73  write_output(std::io::stderr(), &output.stderr, &header)?;
74
75  if !output.status.success() {
76    return Err(std::io::Error::other(format!(
77      "Failed to run '{args:?}'\n\t{}",
78      String::from_utf8_lossy(&output.stderr)
79    )));
80  }
81
82  Ok(output)
83}
84
85pub fn build_js(path: impl AsRef<Path>) -> Result<()> {
86  let path = path.as_ref().to_string_lossy().to_string();
87  let strict_offline: bool = matches!(
88    std::env::var("PNPM_OFFLINE").as_deref(),
89    Ok("TRUE") | Ok("true") | Ok("1")
90  );
91
92  // Note that `--frozen-lockfile` and `-ignore-workspace` are practically exclusive
93  // Because ignoring the workspace one, will require to create a new lockfile.
94  let out_dir = std::env::var("OUT_DIR").unwrap();
95  let build_result = if out_dir.contains("target/package") {
96    // When we build cargo packages, we cannot rely on the workspace itself and prior installs.
97    pnpm_run(&["--dir", &path, "install", "--ignore-workspace"])
98  } else {
99    // `trailbase-assets` and `trailbase-js` both build JS packages. We've seen issues with
100    // parallel installs in the past. Our current approach is to recommend installing workspace
101    // JS deps upfront in combination with `--prefer-offline`. We used to use plain `--offline`,
102    // however this adds an extra mandatory step when vendoring trailbase for framework use-cases.
103    let args = if strict_offline {
104      ["--dir", &path, "install", "--frozen-lockfile", "--offline"]
105    } else {
106      [
107        "--dir",
108        &path,
109        "install",
110        "--prefer-frozen-lockfile",
111        "--prefer-offline",
112      ]
113    };
114    let build_result = pnpm_run(&args);
115    if build_result.is_err() {
116      error!(
117        "`pnpm {}` failed. Make sure to install all JS deps first using `pnpm install`",
118        args.join(" ")
119      )
120    }
121    build_result
122  };
123
124  let _ = build_result?;
125
126  let build_output = pnpm_run(&["--dir", &path, "build"]);
127  if build_output.is_err() && cfg!(windows) {
128    error!(
129      "pnpm build failed on Windows. Make sure to enable symlinks: `git config core.symlinks true && git reset --hard`."
130    );
131  }
132
133  let _ = build_output?;
134
135  return Ok(());
136}
137
138pub fn rerun_if_changed(path: impl AsRef<Path>) {
139  let path_str = path.as_ref().to_string_lossy().to_string();
140  // WARN: watching non-existent paths will also trigger rebuilds.
141  if !std::fs::exists(path).unwrap_or(false) {
142    panic!("Path '{path_str}' doesn't exist");
143  }
144  println!("cargo::rerun-if-changed={path_str}");
145}
146
147pub fn init_env_logger() {
148  env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));
149}