Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Codegen/IDL 1: add more build tools #2362

Merged
merged 5 commits into from
Jun 14, 2023
Merged
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
2 changes: 1 addition & 1 deletion .github/workflows/labels.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,4 @@ jobs:
with:
mode: minimum
count: 1
labels: "📊 analytics, 🪳 bug, 🧑‍💻 dev experience, dependencies, 📖 documentation, 💬 discussion, examples, 📉 performance, 🐍 python API, ⛃ re_datastore, 📺 re_viewer, 🔺 re_renderer, 🚜 refactor, ⛴ release, 🦀 rust SDK, 🔨 testing, ui, 🕸️ web"
labels: "📊 analytics, 🪳 bug, codegen/idl, 🧑‍💻 dev experience, dependencies, 📖 documentation, 💬 discussion, examples, 📉 performance, 🐍 python API, ⛃ re_datastore, 📺 re_viewer, 🔺 re_renderer, 🚜 refactor, ⛴ release, 🦀 rust SDK, 🔨 testing, ui, 🕸️ web"
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
**/target_wasm

# Python virtual environment:
/venv
**/venv

# Python build artifacts:
__pycache__
Expand Down
3 changes: 3 additions & 0 deletions Cargo.lock

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

3 changes: 3 additions & 0 deletions crates/re_build_tools/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,7 @@ all-features = true
anyhow.workspace = true
cargo_metadata = "0.15"
glob = "0.3"
sha2 = "0.10"
time = { workspace = true, features = ["formatting"] }
unindent = "0.1"
walkdir = "2.0"
181 changes: 181 additions & 0 deletions crates/re_build_tools/src/hashing.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
use std::fmt::Write;
use std::path::{Path, PathBuf};
use std::{fs, io};

use anyhow::Context as _;
use sha2::{Digest, Sha256};

use crate::{rerun_if_changed, rerun_if_changed_or_doesnt_exist};

// ---

fn encode_hex(bytes: &[u8]) -> String {
let mut s = String::with_capacity(bytes.len() * 2);
for &b in bytes {
write!(&mut s, "{b:02x}").unwrap();
}
s
}

/// Walks the directory at `path` in filename order.
///
/// If `extensions` is specified, only files with the right extensions will be iterated.
/// Specified extensions should _not_ include the leading dot, e.g. `fbs` rather than `.fbs`.
pub fn iter_dir<'a>(
path: impl AsRef<Path>,
extensions: Option<&'a [&'a str]>,
) -> impl Iterator<Item = PathBuf> + 'a {
fn filter(entry: &walkdir::DirEntry, extensions: Option<&[&str]>) -> bool {
let is_dir = entry.file_type().is_dir();
let is_interesting = extensions.map_or(true, |extensions| {
extensions.iter().any(|ext| {
entry
.path()
.extension()
.map_or(false, |ext2| *ext == ext2.to_string_lossy())
})
});
is_dir || is_interesting
}

let path = path.as_ref();
walkdir::WalkDir::new(path)
.sort_by_file_name()
.into_iter()
.filter_entry(move |entry| filter(entry, extensions))
.filter_map(|entry| entry.ok())
.filter_map(|entry| entry.file_type().is_file().then(|| entry.into_path()))
}

/// Given a file path, computes the sha256 hash of its contents and returns an hexadecimal string
/// for it.
///
/// This will automatically emit a `rerun-if-changed` clause for the specified file.
///
/// Panics if the file doesn't exist.
pub fn compute_file_hash(path: impl AsRef<Path>) -> String {
teh-cmc marked this conversation as resolved.
Show resolved Hide resolved
let mut hasher = Sha256::new();

let path = path.as_ref();
let mut file = fs::File::open(path)
.with_context(|| format!("couldn't open {path:?}"))
.unwrap();
io::copy(&mut file, &mut hasher)
.with_context(|| format!("couldn't copy from {path:?}"))
.unwrap();

rerun_if_changed(path);

encode_hex(hasher.finalize().as_slice())
}

/// Given a directory path, computes the sha256 hash of the accumulated contents of all of its
/// files (ordered by filename), and returns an hexadecimal string for it.
///
/// This includes files in sub-directories (i.e. it's recursive).
///
/// This will automatically emit a `rerun-if-changed` clause for all the files that were hashed.
///
/// If `extensions` is specified, only files with the right extensions will be iterated.
/// Specified extensions should _not_ include the leading dot, e.g. `fbs` rather than `.fbs`.
pub fn compute_dir_hash<'a>(path: impl AsRef<Path>, extensions: Option<&'a [&'a str]>) -> String {
let mut hasher = Sha256::new();

let path = path.as_ref();
for filepath in iter_dir(path, extensions) {
let mut file = fs::File::open(&filepath)
teh-cmc marked this conversation as resolved.
Show resolved Hide resolved
.with_context(|| format!("couldn't open {filepath:?}"))
.unwrap();
io::copy(&mut file, &mut hasher)
.with_context(|| format!("couldn't copy from {filepath:?}"))
.unwrap();

rerun_if_changed(path);
}

encode_hex(hasher.finalize().as_slice())
}

/// Given a crate name, computes the sha256 hash of its source code (ordered by filename) and
/// returns an hexadecimal string for it.
///
/// This includes the source code of all its direct and indirect dependencies.
///
/// This will automatically emit a `rerun-if-changed` clause for all the files that were hashed.
pub fn compute_crate_hash(pkg_name: impl AsRef<str>) -> String {
use cargo_metadata::{CargoOpt, MetadataCommand};
let metadata = MetadataCommand::new()
.features(CargoOpt::AllFeatures)
.exec()
.unwrap();

let pkg_name = pkg_name.as_ref();
let mut files = Default::default();

let pkgs = crate::Packages::from_metadata(&metadata);
pkgs.track_implicit_dep(pkg_name, &mut files);

let mut files = files.into_iter().collect::<Vec<_>>();
files.sort();

let hashes = files.into_iter().map(compute_file_hash).collect::<Vec<_>>();
let hashes = hashes.iter().map(|s| s.as_str()).collect::<Vec<_>>();

compute_strings_hash(&hashes)
}

/// Given a bunch of strings, computes the sha256 hash of their contents (in the order they
/// were passed in) and returns an hexadecimal string for it.
pub fn compute_strings_hash(strs: &[&str]) -> String {
let mut hasher = Sha256::new();

for s in strs {
hasher.update(s);
}

encode_hex(hasher.finalize().as_slice())
}

/// Writes the given `hash` at the specified `path`.
///
/// `hash` should have been computed using of the methods in this module: [`compute_file_hash`],
/// [`compute_dir_hash`], [`compute_crate_hash`].
///
/// Panics on I/O errors.
///
/// Use [`read_versioning_hash`] to read it back.
pub fn write_versioning_hash(path: impl AsRef<Path>, hash: impl AsRef<str>) {
let path = path.as_ref();
let hash = hash.as_ref();

let contents = unindent::unindent(&format!(
"
# This is a sha256 hash for all direct and indirect dependencies of this crate's build script.
teh-cmc marked this conversation as resolved.
Show resolved Hide resolved
# It can be safely removed at anytime to force the build script to run again.
# Check out build.rs to see how it's computed.
{hash}
"
));
teh-cmc marked this conversation as resolved.
Show resolved Hide resolved
std::fs::write(path, contents.trim())
.with_context(|| format!("couldn't write to {path:?}"))
.unwrap();
}

/// Reads back a versioning hash that was written with [`write_versioning_hash`].
///
/// This will automatically emit a `rerun-if-changed` clause for the specified filepath.
///
/// Returns `None` on error.
pub fn read_versioning_hash(path: impl AsRef<Path>) -> Option<String> {
let path = path.as_ref();
teh-cmc marked this conversation as resolved.
Show resolved Hide resolved

// NOTE: It's important we trigger if the file doesn't exist, as this means the user explicitly
// deleted the versioning file, i.e. they're trying to force a rebuild.
rerun_if_changed_or_doesnt_exist(path);

std::fs::read_to_string(path).ok().and_then(|contents| {
contents
.lines()
.find_map(|line| (!line.trim().starts_with('#')).then(|| line.trim().to_owned()))
})
}
9 changes: 8 additions & 1 deletion crates/re_build_tools/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,16 @@ use anyhow::Context as _;

use std::process::Command;

mod hashing;
mod rebuild_detector;

pub use rebuild_detector::{
pub(crate) use self::rebuild_detector::Packages;

pub use self::hashing::{
compute_crate_hash, compute_dir_hash, compute_file_hash, compute_strings_hash, iter_dir,
read_versioning_hash, write_versioning_hash,
};
pub use self::rebuild_detector::{
get_and_track_env_var, is_tracked_env_var_set, rebuild_if_crate_changed, rerun_if_changed,
rerun_if_changed_glob, rerun_if_changed_or_doesnt_exist, write_file_if_necessary,
};
Expand Down
2 changes: 1 addition & 1 deletion crates/re_build_tools/src/rebuild_detector.rs
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ pub fn write_file_if_necessary(

// ---

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

Expand Down