Skip to content

Commit

Permalink
Auto merge of rust-lang#16840 - Wilfred:shell_runnable, r=Veykril
Browse files Browse the repository at this point in the history
Allow rust-project.json to include arbitrary shell commands for runnables

This is a follow-up on rust-lang#16135, resolving the feedback raised :)

Allow rust-project.json to include shell runnables, of the form:

```
{
  "build_info": {
    "label": "//project/foo:my-crate",
    "target_kind": "bin",
    "shell_runnables": [
      {
        "kind": "run",
        "program": "buck2",
        "args": ["run", "//project/foo:my-crate"]
      },
      {
        "kind": "test_one",
        "program": "test_runner",
        "args": ["--name=$$TEST_NAME$$"]
      }
    ]
  }
}

```

If these runnable configs are present for the current crate in rust-project.json, offer them as runnables in VS Code.

This PR required some boring changes to APIs that previously only handled cargo situations. I've split out these changes as commits labelled 'refactor', so it's easy to see the interesting changes.
  • Loading branch information
bors committed Jun 11, 2024
2 parents 5e1ab70 + 1e9e86c commit a97aef8
Show file tree
Hide file tree
Showing 17 changed files with 631 additions and 230 deletions.
2 changes: 1 addition & 1 deletion src/tools/rust-analyzer/crates/project-model/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ mod cargo_workspace;
mod cfg;
mod env;
mod manifest_path;
mod project_json;
pub mod project_json;
mod rustc_cfg;
mod sysroot;
pub mod target_data_layout;
Expand Down
177 changes: 173 additions & 4 deletions src/tools/rust-analyzer/crates/project-model/src/project_json.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
//!
//! * file on disk
//! * a field in the config (ie, you can send a JSON request with the contents
//! of rust-project.json to rust-analyzer, no need to write anything to disk)
//! of `rust-project.json` to rust-analyzer, no need to write anything to disk)
//!
//! Another possible thing we don't do today, but which would be totally valid,
//! is to add an extension point to VS Code extension to register custom
Expand All @@ -55,8 +55,7 @@ use rustc_hash::FxHashMap;
use serde::{de, Deserialize, Serialize};
use span::Edition;

use crate::cfg::CfgFlag;
use crate::ManifestPath;
use crate::{cfg::CfgFlag, ManifestPath, TargetKind};

/// Roots and crates that compose this Rust project.
#[derive(Clone, Debug, Eq, PartialEq)]
Expand All @@ -68,6 +67,10 @@ pub struct ProjectJson {
project_root: AbsPathBuf,
manifest: Option<ManifestPath>,
crates: Vec<Crate>,
/// Configuration for CLI commands.
///
/// Examples include a check build or a test run.
runnables: Vec<Runnable>,
}

/// A crate points to the root module of a crate and lists the dependencies of the crate. This is
Expand All @@ -88,13 +91,94 @@ pub struct Crate {
pub(crate) exclude: Vec<AbsPathBuf>,
pub(crate) is_proc_macro: bool,
pub(crate) repository: Option<String>,
pub build: Option<Build>,
}

/// Additional, build-specific data about a crate.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Build {
/// The name associated with this crate.
///
/// This is determined by the build system that produced
/// the `rust-project.json` in question. For instance, if buck were used,
/// the label might be something like `//ide/rust/rust-analyzer:rust-analyzer`.
///
/// Do not attempt to parse the contents of this string; it is a build system-specific
/// identifier similar to [`Crate::display_name`].
pub label: String,
/// Path corresponding to the build system-specific file defining the crate.
///
/// It is roughly analogous to [`ManifestPath`], but it should *not* be used with
/// [`crate::ProjectManifest::from_manifest_file`], as the build file may not be
/// be in the `rust-project.json`.
pub build_file: Utf8PathBuf,
/// The kind of target.
///
/// Examples (non-exhaustively) include [`TargetKind::Bin`], [`TargetKind::Lib`],
/// and [`TargetKind::Test`]. This information is used to determine what sort
/// of runnable codelens to provide, if any.
pub target_kind: TargetKind,
}

/// A template-like structure for describing runnables.
///
/// These are used for running and debugging binaries and tests without encoding
/// build system-specific knowledge into rust-analyzer.
///
/// # Example
///
/// Below is an example of a test runnable. `{label}` and `{test_id}`
/// are explained in [`Runnable::args`]'s documentation.
///
/// ```json
/// {
/// "program": "buck",
/// "args": [
/// "test",
/// "{label}",
/// "--",
/// "{test_id}",
/// "--print-passing-details"
/// ],
/// "cwd": "/home/user/repo-root/",
/// "kind": "testOne"
/// }
/// ```
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Runnable {
/// The program invoked by the runnable.
///
/// For example, this might be `cargo`, `buck`, or `bazel`.
pub program: String,
/// The arguments passed to [`Runnable::program`].
///
/// The args can contain two template strings: `{label}` and `{test_id}`.
/// rust-analyzer will find and replace `{label}` with [`Build::label`] and
/// `{test_id}` with the test name.
pub args: Vec<String>,
/// The current working directory of the runnable.
pub cwd: Utf8PathBuf,
pub kind: RunnableKind,
}

/// The kind of runnable.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RunnableKind {
Check,

/// Can run a binary.
Run,

/// Run a single test.
TestOne,
}

impl ProjectJson {
/// Create a new ProjectJson instance.
///
/// # Arguments
///
/// * `manifest` - The path to the `rust-project.json`.
/// * `base` - The path to the workspace root (i.e. the folder containing `rust-project.json`)
/// * `data` - The parsed contents of `rust-project.json`, or project json that's passed via
/// configuration.
Expand All @@ -109,6 +193,7 @@ impl ProjectJson {
sysroot_src: data.sysroot_src.map(absolutize_on_base),
project_root: base.to_path_buf(),
manifest,
runnables: data.runnables.into_iter().map(Runnable::from).collect(),
crates: data
.crates
.into_iter()
Expand All @@ -127,6 +212,15 @@ impl ProjectJson {
None => (vec![root_module.parent().unwrap().to_path_buf()], Vec::new()),
};

let build = match crate_data.build {
Some(build) => Some(Build {
label: build.label,
build_file: build.build_file,
target_kind: build.target_kind.into(),
}),
None => None,
};

Crate {
display_name: crate_data
.display_name
Expand All @@ -146,6 +240,7 @@ impl ProjectJson {
exclude,
is_proc_macro: crate_data.is_proc_macro,
repository: crate_data.repository,
build,
}
})
.collect(),
Expand All @@ -167,7 +262,15 @@ impl ProjectJson {
&self.project_root
}

/// Returns the path to the project's manifest file, if it exists.
pub fn crate_by_root(&self, root: &AbsPath) -> Option<Crate> {
self.crates
.iter()
.filter(|krate| krate.is_workspace_member)
.find(|krate| krate.root_module == root)
.cloned()
}

/// Returns the path to the project's manifest, if it exists.
pub fn manifest(&self) -> Option<&ManifestPath> {
self.manifest.as_ref()
}
Expand All @@ -176,13 +279,19 @@ impl ProjectJson {
pub fn manifest_or_root(&self) -> &AbsPath {
self.manifest.as_ref().map_or(&self.project_root, |manifest| manifest.as_ref())
}

pub fn runnables(&self) -> &[Runnable] {
&self.runnables
}
}

#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct ProjectJsonData {
sysroot: Option<Utf8PathBuf>,
sysroot_src: Option<Utf8PathBuf>,
crates: Vec<CrateData>,
#[serde(default)]
runnables: Vec<RunnableData>,
}

#[derive(Serialize, Deserialize, Debug, Clone)]
Expand All @@ -205,6 +314,8 @@ struct CrateData {
is_proc_macro: bool,
#[serde(default)]
repository: Option<String>,
#[serde(default)]
build: Option<BuildData>,
}

#[derive(Serialize, Deserialize, Debug, Clone)]
Expand All @@ -220,6 +331,48 @@ enum EditionData {
Edition2024,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BuildData {
label: String,
build_file: Utf8PathBuf,
target_kind: TargetKindData,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RunnableData {
pub program: String,
pub args: Vec<String>,
pub cwd: Utf8PathBuf,
pub kind: RunnableKindData,
}

#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub enum RunnableKindData {
Check,
Run,
TestOne,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub enum TargetKindData {
Bin,
/// Any kind of Cargo lib crate-type (dylib, rlib, proc-macro, ...).
Lib,
Test,
}

impl From<TargetKindData> for TargetKind {
fn from(data: TargetKindData) -> Self {
match data {
TargetKindData::Bin => TargetKind::Bin,
TargetKindData::Lib => TargetKind::Lib { is_proc_macro: false },
TargetKindData::Test => TargetKind::Test,
}
}
}

impl From<EditionData> for Edition {
fn from(data: EditionData) -> Self {
match data {
Expand All @@ -231,6 +384,22 @@ impl From<EditionData> for Edition {
}
}

impl From<RunnableData> for Runnable {
fn from(data: RunnableData) -> Self {
Runnable { program: data.program, args: data.args, cwd: data.cwd, kind: data.kind.into() }
}
}

impl From<RunnableKindData> for RunnableKind {
fn from(data: RunnableKindData) -> Self {
match data {
RunnableKindData::Check => RunnableKind::Check,
RunnableKindData::Run => RunnableKind::Run,
RunnableKindData::TestOne => RunnableKind::TestOne,
}
}
}

/// Identifies a crate by position in the crates array.
///
/// This will differ from `CrateId` when multiple `ProjectJson`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ pub enum ProjectWorkspaceKind {
/// Environment variables set in the `.cargo/config` file.
cargo_config_extra_env: FxHashMap<String, String>,
},
/// Project workspace was manually specified using a `rust-project.json` file.
/// Project workspace was specified using a `rust-project.json` file.
Json(ProjectJson),
// FIXME: The primary limitation of this approach is that the set of detached files needs to be fixed at the beginning.
// That's not the end user experience we should strive for.
Expand Down
61 changes: 45 additions & 16 deletions src/tools/rust-analyzer/crates/rust-analyzer/src/global_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,7 @@ use parking_lot::{
RwLockWriteGuard,
};
use proc_macro_api::ProcMacroServer;
use project_model::{
CargoWorkspace, ManifestPath, ProjectWorkspace, ProjectWorkspaceKind, Target,
WorkspaceBuildScripts,
};
use project_model::{ManifestPath, ProjectWorkspace, ProjectWorkspaceKind, WorkspaceBuildScripts};
use rustc_hash::{FxHashMap, FxHashSet};
use tracing::{span, Level};
use triomphe::Arc;
Expand All @@ -40,6 +37,7 @@ use crate::{
mem_docs::MemDocs,
op_queue::OpQueue,
reload,
target_spec::{CargoTargetSpec, ProjectJsonTargetSpec, TargetSpec},
task_pool::{TaskPool, TaskQueue},
};

Expand Down Expand Up @@ -556,21 +554,52 @@ impl GlobalStateSnapshot {
self.vfs_read().file_path(file_id).clone()
}

pub(crate) fn cargo_target_for_crate_root(
&self,
crate_id: CrateId,
) -> Option<(&CargoWorkspace, Target)> {
pub(crate) fn target_spec_for_crate(&self, crate_id: CrateId) -> Option<TargetSpec> {
let file_id = self.analysis.crate_root(crate_id).ok()?;
let path = self.vfs_read().file_path(file_id).clone();
let path = path.as_path()?;
self.workspaces.iter().find_map(|ws| match &ws.kind {
ProjectWorkspaceKind::Cargo { cargo, .. }
| ProjectWorkspaceKind::DetachedFile { cargo: Some((cargo, _)), .. } => {
cargo.target_by_root(path).map(|it| (cargo, it))
}
ProjectWorkspaceKind::Json { .. } => None,
ProjectWorkspaceKind::DetachedFile { .. } => None,
})

for workspace in self.workspaces.iter() {
match &workspace.kind {
ProjectWorkspaceKind::Cargo { cargo, .. }
| ProjectWorkspaceKind::DetachedFile { cargo: Some((cargo, _)), .. } => {
let Some(target_idx) = cargo.target_by_root(path) else {
continue;
};

let target_data = &cargo[target_idx];
let package_data = &cargo[target_data.package];

return Some(TargetSpec::Cargo(CargoTargetSpec {
workspace_root: cargo.workspace_root().to_path_buf(),
cargo_toml: package_data.manifest.clone(),
crate_id,
package: cargo.package_flag(package_data),
target: target_data.name.clone(),
target_kind: target_data.kind,
required_features: target_data.required_features.clone(),
features: package_data.features.keys().cloned().collect(),
}));
}
ProjectWorkspaceKind::Json(project) => {
let Some(krate) = project.crate_by_root(path) else {
continue;
};
let Some(build) = krate.build else {
continue;
};

return Some(TargetSpec::ProjectJson(ProjectJsonTargetSpec {
label: build.label,
target_kind: build.target_kind,
shell_runnables: project.runnables().to_owned(),
}));
}
ProjectWorkspaceKind::DetachedFile { .. } => {}
};
}

None
}

pub(crate) fn file_exists(&self, file_id: FileId) -> bool {
Expand Down
Loading

0 comments on commit a97aef8

Please sign in to comment.