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
29 changes: 29 additions & 0 deletions tools/Cargo.lock

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

1 change: 1 addition & 0 deletions tools/hermes/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ serde = { version = "1.0.228", features = ["derive"] }
serde_json = "1.0.149"
syn = { version = "2.0.114", features = ["full", "visit", "extra-traits", "parsing"] }
thiserror = "2.0.18"
walkdir = "2.5.0"

[dev-dependencies]
syn = { version = "2.0.114", features = ["printing", "full", "visit", "extra-traits", "parsing"] }
Expand Down
11 changes: 9 additions & 2 deletions tools/hermes/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
mod errors;
mod parse;
mod resolve;
mod shadow;
mod transform;
mod ui_test_shim;

Expand All @@ -27,11 +28,14 @@ fn main() {
// TODO: Better error handling than `.unwrap()`.
let roots = resolve::resolve_roots(&args.resolve).unwrap();

// TODO: What artifacts need to be updated (not just copied)? E.g., do we
// need to update `Cargo.toml` to rewrite relative paths?

// TODO: From each root, parse and walk referenced modules.
let mut has_errors = false;
for (package, kind, path) in roots {
for (package, kind, path) in roots.roots {
let mut edits = Vec::new();
let res = parse::read_file_and_visit_hermes_items(path.as_std_path(), |_src, res| {
let res = parse::read_file_and_visit_hermes_items(&path, |_src, res| {
if let Err(e) = res {
has_errors = true;
eprint!("{:?}", miette::Report::new(e));
Expand All @@ -52,4 +56,7 @@ fn main() {
let mut source = source.into_bytes();
transform::apply_edits(&mut source, &edits);
}

// TODO: Create shadow skeleton.
// shadow::create_shadow_skeleton(&roots.workspace, todo!(), todo!(), todo!()).unwrap();
}
54 changes: 49 additions & 5 deletions tools/hermes/src/resolve.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
use std::env;
use std::{env, path::PathBuf};

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

Expand Down Expand Up @@ -96,10 +96,15 @@ impl TryFrom<&TargetKind> for HermesTargetKind {
}
}

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

/// Resolves all verification roots.
///
/// Each entry represents a distinct compilation artifact to be verified.
pub fn resolve_roots(args: &Args) -> Result<Vec<(PackageName, HermesTargetKind, Utf8PathBuf)>> {
pub fn resolve_roots(args: &Args) -> Result<Roots> {
let mut cmd = MetadataCommand::new();

if let Some(path) = &args.manifest.manifest_path {
Expand All @@ -111,10 +116,13 @@ pub fn resolve_roots(args: &Args) -> Result<Vec<(PackageName, HermesTargetKind,
args.features.forward_metadata(&mut cmd);

let metadata = cmd.exec().context("Failed to run 'cargo metadata'")?;
check_for_external_deps(&metadata)?;

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

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

for package in selected_packages {
log::trace!("Scanning package: {}", package.name);

Expand All @@ -126,7 +134,11 @@ pub fn resolve_roots(args: &Args) -> Result<Vec<(PackageName, HermesTargetKind,
}

for (target, kind) in targets {
roots.push((package.name.clone(), kind.clone(), target.src_path.clone()));
roots.roots.push((
package.name.clone(),
kind.clone(),
target.src_path.as_std_path().to_owned(),
));
}
}

Expand Down Expand Up @@ -254,3 +266,35 @@ fn resolve_targets<'a>(

Ok(selected_artifacts)
}

// TODO: Eventually, we'll want to support external path dependencies by
// rewriting the path in the `Cargo.toml` in the shadow copy.

/// Scans the package graph to ensure all local dependencies are contained
/// within the workspace root. Returns an error if an external path dependency
/// is found.
pub fn check_for_external_deps(metadata: &Metadata) -> Result<()> {
let workspace_root = metadata.workspace_root.as_std_path();

for pkg in &metadata.packages {
// We only care about packages that are "local" (source is None).
// If source is Some(...), it's from crates.io or git, which is fine
// (handled by Cargo).
if pkg.source.is_none() {
let pkg_path = pkg.manifest_path.as_std_path();

// Check if the package lives outside the workspace tree
if !pkg_path.starts_with(workspace_root) {
anyhow::bail!(
"Unsupported external dependency: '{}' at {:?}.\n\
Hermes currently only supports verifying workspaces where all local \
dependencies are contained within the workspace root.",
pkg.name,
pkg_path
);
Comment on lines +288 to +294
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

For more user-friendly error messages, it's better to use .display() for printing paths instead of the Debug format ({:?}). The Debug format for Path includes quotes, which can make the output less clean for users.

Suggested change
anyhow::bail!(
"Unsupported external dependency: '{}' at {:?}.\n\
Hermes currently only supports verifying workspaces where all local \
dependencies are contained within the workspace root.",
pkg.name,
pkg_path
);
anyhow::bail!(
"Unsupported external dependency: '{}' at {}.\n\
Hermes currently only supports verifying workspaces where all local \
dependencies are contained within the workspace root.",
pkg.name,
pkg_path.display()
);

}
}
}

Ok(())
}
59 changes: 59 additions & 0 deletions tools/hermes/src/shadow.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
use std::{
collections::HashSet,
fs,
path::{Path, PathBuf},
};

use anyhow::{Context, Result};
use walkdir::WalkDir;

// TODO: Call this with `skip_paths` set to all `.rs` files which we've found
// via our traversal using syn.

/// Creates a "Shadow Skeleton" of the source project.
pub fn create_shadow_skeleton(
source_root: &Path,
dest_root: &Path,
target_dir: &Path,
skip_paths: &HashSet<PathBuf>,
) -> Result<()> {
let walker = WalkDir::new(source_root)
.follow_links(false) // Security: don't follow symlinks out of the root.
.into_iter();

for entry in walker.filter_entry(|e| e.path() != target_dir && e.file_name() != ".git") {
let entry = entry.context("Failed to read directory entry")?;
let src_path = entry.path();

let relative_path =
src_path.strip_prefix(source_root).context("File is not inside source root")?;
let dest_path = dest_root.join(relative_path);

if entry.file_type().is_dir() {
fs::create_dir_all(&dest_path)
.with_context(|| format!("Failed to create shadow directory: {:?}", dest_path))?;
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

To improve the readability of error messages, consider using .display() for formatting paths instead of the Debug format ({:?}). The Debug format adds quotes around the path, which is often not ideal for user-facing output.

Suggested change
.with_context(|| format!("Failed to create shadow directory: {:?}", dest_path))?;
.with_context(|| format!("Failed to create shadow directory: {}", dest_path.display()))?;

continue;
}

if entry.file_type().is_file() && !skip_paths.contains(src_path) {
make_symlink(src_path, &dest_path)?;
}
}

Ok(())
}

#[cfg(unix)]
fn make_symlink(original: &Path, link: &Path) -> Result<()> {
std::os::unix::fs::symlink(original, link)
.with_context(|| format!("Failed to symlink {:?} -> {:?}", original, link))
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

For cleaner and more user-friendly error messages, it's preferable to use .display() to format paths instead of the Debug format ({:?}).

Suggested change
.with_context(|| format!("Failed to symlink {:?} -> {:?}", original, link))
.with_context(|| format!("Failed to symlink {} -> {}", original.display(), link.display()))

}

#[cfg(windows)]
fn make_symlink(original: &Path, link: &Path) -> Result<()> {
// Windows treats file and directory symlinks differently.
// Since we only call this on files (checking is_file above),
// we use symlink_file.
std::os::windows::fs::symlink_file(original, link)
.with_context(|| format!("Failed to symlink {:?} -> {:?}", original, link))
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

To improve the readability of error messages, consider using .display() for formatting paths instead of the Debug format ({:?}). This will provide a cleaner output for the user.

Suggested change
.with_context(|| format!("Failed to symlink {:?} -> {:?}", original, link))
.with_context(|| format!("Failed to symlink {} -> {}", original.display(), link.display()))

}
1 change: 1 addition & 0 deletions tools/vendor/same-file/.cargo-checksum.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"files":{".cargo_vcs_info.json":"6dd5cd03c25ddfc57cd9bd43fcd964b24fecb903f60fd1568f092121b22fee93","COPYING":"01c266bced4a434da0051174d6bee16a4c82cf634e2679b6155d40d75012390f","Cargo.lock":"fa40407b035c7abffe97d267d2ff95d22d83e5b916aca876bec49a56a9067c73","Cargo.toml":"991f8df8fa5a259801900a56908cf21a66c5cf7b238bc81ba9bdf348e233252e","Cargo.toml.orig":"d0b19f99d598bf88dd101f441f577d7a82455cf85d1f44aa0771e1d1f633da45","LICENSE-MIT":"cb3c929a05e6cbc9de9ab06a4c57eeb60ca8c724bef6c138c87d3a577e27aa14","README.md":"70c109d9c89b4479016142f2a4ad6963b6fe5793bcdd997add3d3af3d2baf36b","UNLICENSE":"7e12e5df4bae12cb21581ba157ced20e1986a0508dd10d0e8a4ab9a4cf94e85c","examples/is_same_file.rs":"7b3eeb27a15051667d97615fc7a2339cbff5630df3bca6ac19ab81d5be22f329","examples/is_stderr.rs":"e1c5d1a0f36d7aa0020bb5b87c2f45c7176033f03c52cf395be55dd8debfc413","rustfmt.toml":"1ca600239a27401c4a43f363cf3f38183a212affc1f31bff3ae93234bbaec228","src/lib.rs":"b22c2f0b5cad2248f16f4f42add52b2dc0c627631f71ee67a8c38fe305048f85","src/unix.rs":"69abed9fade151247696c6d4a442ef299554f3722e23a2d08053598a52a27d62","src/unknown.rs":"bfde4e9ac88f500c0ccb69165383682ddd24bf7d7ddaf5859426e1fd4b2f9359","src/win.rs":"94f912cc3734f60608d0ee2b0c664afb65fc96e5b0b223a53565fb8998c03fa3"},"package":"93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"}
5 changes: 5 additions & 0 deletions tools/vendor/same-file/.cargo_vcs_info.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"git": {
"sha1": "5799cd323b8eefd17a089c950dac113f66c89c9e"
}
}
3 changes: 3 additions & 0 deletions tools/vendor/same-file/COPYING
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
This project is dual-licensed under the Unlicense and MIT licenses.

You may use this code under the terms of either license.
48 changes: 48 additions & 0 deletions tools/vendor/same-file/Cargo.lock

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

29 changes: 29 additions & 0 deletions tools/vendor/same-file/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
#
# When uploading crates to the registry Cargo will automatically
# "normalize" Cargo.toml files for maximal compatibility
# with all versions of Cargo and also rewrite `path` dependencies
# to registry (e.g., crates.io) dependencies
#
# If you believe there's an error in this file please file an
# issue against the rust-lang/cargo repository. If you're
# editing this file be aware that the upstream Cargo.toml
# will likely look very different (and much more reasonable)

[package]
edition = "2018"
name = "same-file"
version = "1.0.6"
authors = ["Andrew Gallant <jamslam@gmail.com>"]
exclude = ["/.github"]
description = "A simple crate for determining whether two file paths point to the same file.\n"
homepage = "https://github.com/BurntSushi/same-file"
documentation = "https://docs.rs/same-file"
readme = "README.md"
keywords = ["same", "file", "equal", "inode"]
license = "Unlicense/MIT"
repository = "https://github.com/BurntSushi/same-file"
[dev-dependencies.doc-comment]
version = "0.3"
[target."cfg(windows)".dependencies.winapi-util]
version = "0.1.1"
21 changes: 21 additions & 0 deletions tools/vendor/same-file/Cargo.toml.orig

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

21 changes: 21 additions & 0 deletions tools/vendor/same-file/LICENSE-MIT
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
The MIT License (MIT)

Copyright (c) 2017 Andrew Gallant

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
Loading
Loading