Skip to content

Commit

Permalink
rust_analyzer: make all paths in rust-project.json absolute (#3033)
Browse files Browse the repository at this point in the history
This is a step towards supporting automatic project discovery (#2755).

Relative paths are resolved against the location of rust-project.json,
but in auto-discovery this is the location of the BUILD file being
discovered, not the workspace root.

We already use absolute paths for generated files, so rust-project.json
is not workspace-independent today.

The alternatives seem more complex for little gain:

- Only use absolute paths for auto-discovery mode, relative otherwise.
- Use ../../ to express all workspace paths relative to the BUILD.

See
#2755 (comment)
  • Loading branch information
sam-mccall authored Dec 2, 2024
1 parent ba6af15 commit 74f164b
Show file tree
Hide file tree
Showing 9 changed files with 128 additions and 74 deletions.
17 changes: 10 additions & 7 deletions rust/private/rust_analyzer.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,9 @@ rust_analyzer_aspect = aspect(
doc = "Annotates rust rules with RustAnalyzerInfo later used to build a rust-project.json",
)

# Paths in the generated JSON file begin with one of these placeholders.
# The gen_rust_project driver will replace them with absolute paths.
_WORKSPACE_TEMPLATE = "__WORKSPACE__/"
_EXEC_ROOT_TEMPLATE = "__EXEC_ROOT__/"
_OUTPUT_BASE_TEMPLATE = "__OUTPUT_BASE__/"

Expand Down Expand Up @@ -222,7 +225,7 @@ def _create_single_crate(ctx, attrs, info):
# TODO: Some folks may want to override this for vendored dependencies.
is_external = info.crate.root.path.startswith("external/")
is_generated = not info.crate.root.is_source
path_prefix = _EXEC_ROOT_TEMPLATE if is_external or is_generated else ""
path_prefix = _EXEC_ROOT_TEMPLATE if is_external or is_generated else _WORKSPACE_TEMPLATE
crate["is_workspace_member"] = not is_external
crate["root_module"] = path_prefix + info.crate.root.path
crate["source"] = {"exclude_dirs": [], "include_dirs": []}
Expand All @@ -231,7 +234,7 @@ def _create_single_crate(ctx, attrs, info):
srcs = getattr(ctx.rule.files, "srcs", [])
src_map = {src.short_path: src for src in srcs if src.is_source}
if info.crate.root.short_path in src_map:
crate["root_module"] = src_map[info.crate.root.short_path].path
crate["root_module"] = _WORKSPACE_TEMPLATE + src_map[info.crate.root.short_path].path
crate["source"]["include_dirs"].append(path_prefix + info.crate.root.dirname)

if info.build_info != None and info.build_info.out_dir != None:
Expand Down Expand Up @@ -263,7 +266,8 @@ def _create_single_crate(ctx, attrs, info):
crate["deps"] = [_crate_id(dep.crate) for dep in info.deps if _crate_id(dep.crate) != crate_id]
crate["aliases"] = {_crate_id(alias_target.crate): alias_name for alias_target, alias_name in info.aliases.items()}
crate["cfg"] = info.cfgs
crate["target"] = find_toolchain(ctx).target_flag_value
toolchain = find_toolchain(ctx)
crate["target"] = (_EXEC_ROOT_TEMPLATE + toolchain.target_json.path) if toolchain.target_json else toolchain.target_flag_value
if info.proc_macro_dylib_path != None:
crate["proc_macro_dylib_path"] = _EXEC_ROOT_TEMPLATE + info.proc_macro_dylib_path
return crate
Expand Down Expand Up @@ -315,6 +319,8 @@ def _rust_analyzer_detect_sysroot_impl(ctx):
sysroot_src = rustc_srcs.label.package + "/library"
if rustc_srcs.label.workspace_root:
sysroot_src = _OUTPUT_BASE_TEMPLATE + rustc_srcs.label.workspace_root + "/" + sysroot_src
else:
sysroot_src = _WORKSPACE_TEMPLATE + sysroot_src

rustc = rust_analyzer_toolchain.rustc
sysroot_dir, _, bin_dir = rustc.dirname.rpartition("/")
Expand All @@ -323,10 +329,7 @@ def _rust_analyzer_detect_sysroot_impl(ctx):
rustc.path,
))

sysroot = "{}/{}".format(
_OUTPUT_BASE_TEMPLATE,
sysroot_dir,
)
sysroot = _OUTPUT_BASE_TEMPLATE + sysroot_dir

toolchain_info = {
"sysroot": sysroot,
Expand Down
4 changes: 4 additions & 0 deletions test/rust_analyzer/generated_srcs_test/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,8 @@ rust_test(
# contexts outside of `//test/rust_analyzer:rust_analyzer_test`. Run
# that target to execute this test.
tags = ["manual"],
deps = [
"@rules_rust//test/3rdparty/crates:serde",
"@rules_rust//test/3rdparty/crates:serde_json",
],
)
72 changes: 43 additions & 29 deletions test/rust_analyzer/generated_srcs_test/rust_project_json_test.rs
Original file line number Diff line number Diff line change
@@ -1,43 +1,57 @@
#[cfg(test)]
mod tests {
use serde::Deserialize;
use std::env;
use std::path::PathBuf;

#[derive(Deserialize)]
struct Project {
sysroot_src: String,
crates: Vec<Crate>,
}

#[derive(Deserialize)]
struct Crate {
display_name: String,
root_module: String,
source: Option<Source>,
}

#[derive(Deserialize)]
struct Source {
include_dirs: Vec<String>,
}

#[test]
fn test_deps_of_crate_and_its_test_are_merged() {
fn test_generated_srcs() {
let rust_project_path = PathBuf::from(env::var("RUST_PROJECT_JSON").unwrap());

let content = std::fs::read_to_string(&rust_project_path)
.unwrap_or_else(|_| panic!("couldn't open {:?}", &rust_project_path));
println!("{}", content);
let project: Project =
serde_json::from_str(&content).expect("Failed to deserialize project JSON");

let output_base = content
.lines()
.find(|text| text.trim_start().starts_with("\"sysroot_src\":"))
.map(|text| {
let mut split = text.splitn(2, "\"sysroot_src\": ");
let mut with_hash = split.nth(1).unwrap().trim().splitn(2, "/external/");
let mut output = with_hash.next().unwrap().rsplitn(2, '/');
output.nth(1).unwrap()
})
.expect("Failed to find sysroot entry.");
// /tmp/_bazel/12345678/external/tools/rustlib/library => /tmp/_bazel
let output_base = project
.sysroot_src
.rsplitn(2, "/external/")
.last()
.unwrap()
.rsplitn(2, '/')
.last()
.unwrap();
println!("output_base: {output_base}");

let expected = r#"{
"display_name": "generated_srcs",
"root_module": "lib.rs",
"edition": "2021",
"deps": [],
"is_workspace_member": true,
"source": {
"include_dirs": [
"#
.to_owned()
+ output_base;
let gen = project
.crates
.iter()
.find(|c| &c.display_name == "generated_srcs")
.unwrap();
assert!(gen.root_module.starts_with("/"));
assert!(gen.root_module.ends_with("/lib.rs"));

println!("{}", content);
assert!(
content.contains(&expected),
"expected rust-project.json to contain the following block:\n{}",
expected
);
let include_dirs = &gen.source.as_ref().unwrap().include_dirs;
assert!(include_dirs.len() == 1);
assert!(include_dirs[0].starts_with(output_base));
}
}
4 changes: 4 additions & 0 deletions test/rust_analyzer/merging_crates_test/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,8 @@ rust_test(
# contexts outside of `//test/rust_analyzer:rust_analyzer_test`. Run
# that target to execute this test.
tags = ["manual"],
deps = [
"@rules_rust//test/3rdparty/crates:serde",
"@rules_rust//test/3rdparty/crates:serde_json",
],
)
48 changes: 28 additions & 20 deletions test/rust_analyzer/merging_crates_test/rust_project_json_test.rs
Original file line number Diff line number Diff line change
@@ -1,33 +1,41 @@
#[cfg(test)]
mod tests {
use serde::Deserialize;
use std::env;
use std::path::PathBuf;

#[derive(Deserialize)]
struct Project {
crates: Vec<Crate>,
}

#[derive(Deserialize)]
struct Crate {
display_name: String,
deps: Vec<Dep>,
}

#[derive(Deserialize)]
struct Dep {
name: String,
}

#[test]
fn test_deps_of_crate_and_its_test_are_merged() {
let rust_project_path = PathBuf::from(env::var("RUST_PROJECT_JSON").unwrap());

let content = std::fs::read_to_string(&rust_project_path)
.unwrap_or_else(|_| panic!("couldn't open {:?}", &rust_project_path));

let expected = r#"{
"display_name": "mylib",
"root_module": "mylib.rs",
"edition": "2018",
"deps": [
{
"crate": 0,
"name": "extra_test_dep"
},
{
"crate": 1,
"name": "lib_dep"
}
],"#;

println!("{}", content);
assert!(
content.contains(expected),
"expected rust-project.json to contain both lib_dep and extra_test_dep in deps of mylib.rs.");
let project: Project =
serde_json::from_str(&content).expect("Failed to deserialize project JSON");

let lib = project
.crates
.iter()
.find(|c| &c.display_name == "mylib")
.unwrap();
let mut deps = lib.deps.iter().map(|d| &d.name).collect::<Vec<_>>();
deps.sort();
assert!(deps == vec!["extra_test_dep", "lib_dep"]);
}
}
4 changes: 4 additions & 0 deletions test/rust_analyzer/static_and_shared_lib_test/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,8 @@ rust_test(
# contexts outside of `//test/rust_analyzer:rust_analyzer_test`. Run
# that target to execute this test.
tags = ["manual"],
deps = [
"@rules_rust//test/3rdparty/crates:serde",
"@rules_rust//test/3rdparty/crates:serde_json",
],
)
Original file line number Diff line number Diff line change
@@ -1,31 +1,41 @@
#[cfg(test)]
mod tests {
use serde::Deserialize;
use std::env;
use std::path::PathBuf;

#[derive(Deserialize)]
struct Project {
crates: Vec<Crate>,
}

#[derive(Deserialize)]
struct Crate {
display_name: String,
root_module: String,
}

#[test]
fn test_deps_of_crate_and_its_test_are_merged() {
fn test_static_and_shared_lib() {
let rust_project_path = PathBuf::from(env::var("RUST_PROJECT_JSON").unwrap());

let content = std::fs::read_to_string(&rust_project_path)
.unwrap_or_else(|_| panic!("couldn't open {:?}", &rust_project_path));

println!("{}", content);
let project: Project =
serde_json::from_str(&content).expect("Failed to deserialize project JSON");

let expected_cdylib = r#"{
"display_name": "greeter_cdylib",
"root_module": "shared_lib.rs","#;
assert!(
content.contains(expected_cdylib),
"expected rust-project.json to contain a rust_shared_library target."
);
let cdylib = project
.crates
.iter()
.find(|c| &c.display_name == "greeter_cdylib")
.unwrap();
assert!(cdylib.root_module.ends_with("/shared_lib.rs"));

let expected_staticlib = r#"{
"display_name": "greeter_staticlib",
"root_module": "static_lib.rs","#;
assert!(
content.contains(expected_staticlib),
"expected rust-project.json to contain a rust_static_library target."
);
let staticlib = project
.crates
.iter()
.find(|c| &c.display_name == "greeter_staticlib")
.unwrap();
assert!(staticlib.root_module.ends_with("/static_lib.rs"));
}
}
1 change: 1 addition & 0 deletions tools/rust_analyzer/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ pub fn write_rust_project(

rust_project::write_rust_project(
rust_project_path.as_ref(),
workspace.as_ref(),
execution_root.as_ref(),
output_base.as_ref(),
&rust_project,
Expand Down
8 changes: 7 additions & 1 deletion tools/rust_analyzer/rust_project.rs
Original file line number Diff line number Diff line change
Expand Up @@ -243,10 +243,15 @@ fn detect_cycle<'a>(

pub fn write_rust_project(
rust_project_path: &Path,
workspace: &Path,
execution_root: &Path,
output_base: &Path,
rust_project: &RustProject,
) -> anyhow::Result<()> {
let workspace = workspace
.to_str()
.ok_or_else(|| anyhow!("workspace is not valid UTF-8"))?;

let execution_root = execution_root
.to_str()
.ok_or_else(|| anyhow!("execution_root is not valid UTF-8"))?;
Expand All @@ -272,7 +277,8 @@ pub fn write_rust_project(
let rust_project_content = serde_json::to_string_pretty(rust_project)?
.replace("${pwd}", execution_root)
.replace("__EXEC_ROOT__", execution_root)
.replace("__OUTPUT_BASE__", output_base);
.replace("__OUTPUT_BASE__", output_base)
.replace("__WORKSPACE__", workspace);

// Write the new rust-project.json file.
std::fs::write(rust_project_path, rust_project_content)?;
Expand Down

0 comments on commit 74f164b

Please sign in to comment.