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
2 changes: 1 addition & 1 deletion tools/hermes/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,5 +58,5 @@ fn main() {
}

// TODO: Create shadow skeleton.
// shadow::create_shadow_skeleton(&roots.workspace, todo!(), todo!(), todo!()).unwrap();
// shadow::create_shadow_skeleton(&roots.workspace, &roots.shadow_root, &roots.cargo_target_dir, todo!()).unwrap();
}
36 changes: 30 additions & 6 deletions tools/hermes/src/resolve.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
use std::{env, path::PathBuf};
use std::{
env,
hash::{Hash as _, Hasher as _},
path::PathBuf,
};

use anyhow::{anyhow, Context, Result};
use cargo_metadata::{
Metadata, MetadataCommand, Package, PackageName, Target, TargetKind,
};
use cargo_metadata::{Metadata, MetadataCommand, Package, PackageName, Target, TargetKind};
use clap::Parser;

#[derive(Parser, Debug)]
Expand Down Expand Up @@ -98,6 +100,8 @@ impl TryFrom<&TargetKind> for HermesTargetKind {

pub struct Roots {
pub workspace: PathBuf,
pub cargo_target_dir: PathBuf,
pub shadow_root: PathBuf,
pub roots: Vec<(PackageName, HermesTargetKind, PathBuf)>,
}

Expand All @@ -120,8 +124,12 @@ pub fn resolve_roots(args: &Args) -> Result<Roots> {

let selected_packages = resolve_packages(&metadata, &args.workspace)?;

let mut roots =
Roots { workspace: metadata.workspace_root.as_std_path().to_owned(), roots: Vec::new() };
let mut roots = Roots {
workspace: metadata.workspace_root.as_std_path().to_owned(),
cargo_target_dir: metadata.target_directory.as_std_path().to_owned(),
shadow_root: resolve_shadow_path(&metadata),
roots: Vec::new(),
};

for package in selected_packages {
log::trace!("Scanning package: {}", package.name);
Expand All @@ -145,6 +153,22 @@ pub fn resolve_roots(args: &Args) -> Result<Roots> {
Ok(roots)
}

fn resolve_shadow_path(metadata: &Metadata) -> PathBuf {
// NOTE: Automatically handles `CARGO_TARGET_DIR` env var.
let target_dir = metadata.target_directory.as_std_path();

// Hash the path to the workspace root to avoid collisions between different
// workspaces using the same target directory.
let workspace_root_hash = {
let mut hasher = std::hash::DefaultHasher::new();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

std::hash::DefaultHasher is not guaranteed to produce the same hash across different Rust versions or platforms. This means that if the Rust toolchain is updated, the generated hash for the workspace path might change. This would result in a new hermes_shadow_* directory being created in your target directory, leaving the old one as an orphaned artifact.

While this is not a correctness issue for a single run (as the shadow directory is cleaned), it can lead to clutter in the build output directory over time. If a stable path is desirable, consider using a stable hashing algorithm. You could use a hasher from a crate like seahash or ahash with a fixed seed, or a cryptographic hash like SHA-2.

hasher.write(b"hermes_shadow_salt");
metadata.workspace_root.hash(&mut hasher);
hasher.finish()
};
Comment on lines +162 to +167
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

std::hash::DefaultHasher is not guaranteed to be stable across different Rust versions, or even between 32-bit and 64-bit platforms. This means the generated shadow path could change unexpectedly, which could cause problems with build caching and reproducibility.

For a build tool where deterministic output is important, it's better to use a stable hashing algorithm. A simple and fast option is FnvHasher from the fnv crate.

Here is an example of how you could use it:

  1. Add fnv to tools/hermes/Cargo.toml:
[dependencies]
# ...
fnv = "1.0"
  1. Update the code to use FnvHasher:
// Add to imports: use fnv::FnvHasher;
let workspace_root_hash = {
    let mut hasher = FnvHasher::default();
    hasher.write(b"hermes_shadow_salt");
    metadata.workspace_root.hash(&mut hasher);
    hasher.finish()
};


target_dir.join(format!("hermes_shadow_{workspace_root_hash}"))
}

/// Resolves which packages to process based on workspace flags and CWD.
fn resolve_packages<'a>(
metadata: &'a Metadata,
Expand Down
5 changes: 5 additions & 0 deletions tools/hermes/src/shadow.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ pub fn create_shadow_skeleton(
target_dir: &Path,
skip_paths: &HashSet<PathBuf>,
) -> Result<()> {
if dest_root.exists() {
std::fs::remove_dir_all(&dest_root)?;
}
std::fs::create_dir_all(&dest_root)?;
Comment on lines +20 to +23
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

The current logic for cleaning the destination directory has a couple of race conditions and bugs that can cause the tool to fail unexpectedly:

  1. If dest_root is a dangling symlink, dest_root.exists() returns false. The code then proceeds to std::fs::create_dir_all, which will fail because the path is already occupied by the symlink.
  2. If dest_root is a file (not a directory), dest_root.exists() returns true, but std::fs::remove_dir_all will fail on Unix-like systems, causing an unhandled error.

A more robust approach is needed to handle all these filesystem states correctly.

Suggested change
if dest_root.exists() {
std::fs::remove_dir_all(&dest_root)?;
}
std::fs::create_dir_all(&dest_root)?;
if dest_root.symlink_metadata().is_ok() {
// The path exists. To ensure a clean state, we remove it.
// This handles files, directories, and symlinks (including dangling ones).
std::fs::remove_dir_all(dest_root).or_else(|_| std::fs::remove_file(dest_root))?;
}
std::fs::create_dir_all(dest_root)?;


let walker = WalkDir::new(source_root)
.follow_links(false) // Security: don't follow symlinks out of the root.
.into_iter();
Expand Down
Loading