дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-09 03:05:40 +02:00
feat(gradle): add Gradle settings with Gradle JVM and distribution (#303)
Adds Settings › Build Tools › Gradle: Gradle JVM (defaults to the Project JDK), distribution (wrapper or local installation with its home) and Gradle user home, stored in the project or global settings. Sync and Gradle tool-window tasks use the configured JVM, launcher and GRADLE_USER_HOME. Run configurations keep their own choices: one that uses the wrapper runs gradlew, one that runs plain `gradle` resolves it from the local installation, then the pinned toolchain, then PATH; an explicit JAVA_HOME in the configuration wins over Gradle JVM. Derived defaults are applied at launch and not saved into run configurations. New Gradle configurations start with the wrapper off when the project uses a local installation. Fixes #232
Цей коміт міститься в:
1 джерело
cdb6cc7c0d
коміт
6c1eba9d82
16 files changed
+434
-19
No files matched your search
@@ -32,6 +32,8 @@ pub struct SyncOptions {
|
||||
pub env: BTreeMap<String, String>,
|
||||
/// Maven launcher replacing the project's `mvnw` / `mvn` (Settings › Maven home).
|
||||
pub maven_command: Option<String>,
|
||||
/// Resolved Gradle launcher from Settings.
|
||||
pub gradle_command: Option<String>,
|
||||
/// Maven options before the goals (`-s`, `-Dmaven.repo.local=`).
|
||||
pub maven_options: Vec<String>,
|
||||
}
|
||||
@@ -268,7 +270,14 @@ pub fn sync_gradle(
|
||||
args.push("--offline".into());
|
||||
}
|
||||
progress(SyncProgress { message: "Loading Gradle tasks".into(), fraction: None });
|
||||
let out = run(&model.command, &args, &root.join(&model.dir), &options.env, cancel, |_| {})?;
|
||||
let out = run(
|
||||
options.gradle_command.as_deref().unwrap_or(&model.command),
|
||||
&args,
|
||||
&root.join(&model.dir),
|
||||
&options.env,
|
||||
cancel,
|
||||
|_| {},
|
||||
)?;
|
||||
let cache = GradleCache {
|
||||
hash: hash_files(&gradle::build_files(root, &model)),
|
||||
at_ms: now_ms(),
|
||||
@@ -282,6 +291,29 @@ pub fn sync_gradle(
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn gradle_sync_uses_resolved_command_and_environment() {
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let root = tmp.path();
|
||||
std::fs::write(root.join("build.gradle"), "").unwrap();
|
||||
let launcher = root.join("configured-gradle");
|
||||
std::fs::write(&launcher, "#!/bin/sh\nprintf '%s\\n' \"$JAVA_HOME\" \"$GRADLE_USER_HOME\" > actual-env\n")
|
||||
.unwrap();
|
||||
std::fs::set_permissions(&launcher, std::fs::Permissions::from_mode(0o755)).unwrap();
|
||||
let options = SyncOptions {
|
||||
gradle_command: Some(launcher.to_string_lossy().into_owned()),
|
||||
env: BTreeMap::from([
|
||||
("JAVA_HOME".into(), "/jdk21".into()),
|
||||
("GRADLE_USER_HOME".into(), "/gradle-cache".into()),
|
||||
]),
|
||||
..Default::default()
|
||||
};
|
||||
sync_gradle(root, &root.join("cache"), &options, &CancellationToken::new(), &|_| {}).unwrap();
|
||||
assert_eq!(std::fs::read_to_string(root.join("actual-env")).unwrap(), "/jdk21\n/gradle-cache\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn caches_merge_and_go_stale() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
|
||||
@@ -563,6 +563,29 @@ pub struct JavaSettings {
|
||||
pub jdtls_vm_args: String,
|
||||
/// Build Tools › Maven.
|
||||
pub maven: MavenSettings,
|
||||
/// Build Tools › Gradle.
|
||||
pub gradle: GradleSettings,
|
||||
}
|
||||
|
||||
/// Build Tools › Gradle.
|
||||
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default, rename_all = "camelCase")]
|
||||
pub struct GradleSettings {
|
||||
/// Empty = Project JDK; otherwise a JDK home.
|
||||
pub jvm: String,
|
||||
pub distribution: GradleDistribution,
|
||||
/// Used only for the local distribution.
|
||||
pub home: String,
|
||||
/// Empty = Gradle's default user home.
|
||||
pub user_home: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum GradleDistribution {
|
||||
#[default]
|
||||
Wrapper,
|
||||
Local,
|
||||
}
|
||||
|
||||
/// Build Tools › Maven.
|
||||
@@ -709,3 +732,25 @@ mod maven_linkage_tests {
|
||||
assert_eq!(serde_json::from_str::<MavenSettings>(&encoded).unwrap(), settings);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod gradle_settings_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn defaults_and_round_trip() {
|
||||
let old: JavaSettings = serde_json::from_str(r#"{"jdk":"/jdk"}"#).unwrap();
|
||||
assert_eq!(old.gradle, GradleSettings::default());
|
||||
for distribution in [GradleDistribution::Wrapper, GradleDistribution::Local] {
|
||||
let mut settings = Settings::default();
|
||||
settings.languages.java.gradle = GradleSettings {
|
||||
jvm: "/jdk21".into(),
|
||||
distribution,
|
||||
home: "/gradle".into(),
|
||||
user_home: "/cache/gradle".into(),
|
||||
};
|
||||
let restored: Settings = serde_json::from_str(&serde_json::to_string(&settings).unwrap()).unwrap();
|
||||
assert_eq!(restored.languages.java.gradle, settings.languages.java.gradle);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -46,7 +46,8 @@ pub fn load(root: &Path, cache: &Path, settings: &Settings) -> BuildModel {
|
||||
}
|
||||
}
|
||||
if let Some(g) = &mut model.gradle {
|
||||
g.env = java_env(jdk.as_ref(), g.java_version);
|
||||
g.env = ide_sdk::apply::gradle_env(root, settings, java_env(jdk.as_ref(), g.java_version));
|
||||
g.command = ide_sdk::apply::gradle_command(root, &root.join(&g.dir), settings).to_string_lossy().into_owned();
|
||||
}
|
||||
// A failed sync of a project no installed JDK can build says so first (with a Download JDK action).
|
||||
let missing = |release: Option<u32>| match jdk {
|
||||
@@ -67,7 +68,7 @@ pub fn load(root: &Path, cache: &Path, settings: &Settings) -> BuildModel {
|
||||
if let Some(g) = &mut model.gradle
|
||||
&& g.sync_error.is_some()
|
||||
{
|
||||
let hint = missing(g.java_version);
|
||||
let hint = if settings.languages.java.gradle.jvm.trim().is_empty() { missing(g.java_version) } else { None };
|
||||
explain(&mut g.sync_error, hint);
|
||||
}
|
||||
model
|
||||
@@ -113,6 +114,7 @@ pub async fn build_sync(
|
||||
maven_command: ide_sdk::apply::maven_command(&root, &settings).map(|p| p.to_string_lossy().into_owned()),
|
||||
maven_options: ide_sdk::apply::maven_options(&root, &settings),
|
||||
maven_settings: settings.languages.java.maven.clone(),
|
||||
gradle_command: current.gradle.as_ref().map(|g| g.command.clone()),
|
||||
};
|
||||
let progress = |p: SyncProgress| {
|
||||
let _ = on_progress.send(p);
|
||||
@@ -267,3 +269,31 @@ pub(crate) fn register(d: &mut Dispatcher) {
|
||||
.await
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod gradle_settings_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn model_for_sync_tasks_and_compile_uses_gradle_settings() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let root = tmp.path();
|
||||
std::fs::write(root.join("build.gradle"), "plugins { id 'java' }").unwrap();
|
||||
let wrapper = root.join(if cfg!(windows) { "gradlew.bat" } else { "gradlew" });
|
||||
std::fs::write(&wrapper, "").unwrap();
|
||||
let mut settings = Settings::default();
|
||||
let default = load(root, &root.join("cache"), &settings).gradle.unwrap();
|
||||
assert_eq!(default.command, wrapper.to_string_lossy());
|
||||
settings.languages.java.gradle.jvm = "jdk21".into();
|
||||
settings.languages.java.gradle.user_home = "user-home".into();
|
||||
settings.languages.java.gradle.distribution = ide_core::settings::GradleDistribution::Local;
|
||||
settings.languages.java.gradle.home = "gradle-install".into();
|
||||
let model = load(root, &root.join("cache"), &settings).gradle.unwrap();
|
||||
assert_eq!(
|
||||
model.command,
|
||||
root.join("gradle-install/bin").join(if cfg!(windows) { "gradle.bat" } else { "gradle" }).to_string_lossy()
|
||||
);
|
||||
assert_eq!(model.env["JAVA_HOME"], root.join("jdk21").to_string_lossy());
|
||||
assert_eq!(model.env["GRADLE_USER_HOME"], root.join("user-home").to_string_lossy());
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,10 @@ pub fn configs(host: &Host, window: &str) -> IdeResult<Vec<RunConfig>> {
|
||||
.collect();
|
||||
all.extend(user);
|
||||
// Project interpreter / package manager / toolchains from Languages & Frameworks.
|
||||
ide_sdk::apply::apply_to_run_configs(&mut all, &root, &settings);
|
||||
// Keep Gradle choices unmodified in the editor: derived defaults must not be saved as explicit env.
|
||||
for config in all.iter_mut().filter(|c| c.kind != "gradle") {
|
||||
ide_sdk::apply::apply_to_run_configs(std::slice::from_mut(config), &root, &settings);
|
||||
}
|
||||
Ok(all)
|
||||
}
|
||||
|
||||
@@ -48,7 +51,11 @@ pub async fn start(host: &Host, window: &str, config: RunConfig, on_output: Emit
|
||||
let settings = project.settings.effective();
|
||||
// Configurations made in the UI name bare tools (`godot`) that may live outside PATH.
|
||||
let mut config = config;
|
||||
ide_sdk::apply::resolve_run_command(&mut config, &project.root, &settings);
|
||||
if config.kind == "gradle" {
|
||||
ide_sdk::apply::apply_to_run_configs(std::slice::from_mut(&mut config), &project.root, &settings);
|
||||
} else {
|
||||
ide_sdk::apply::resolve_run_command(&mut config, &project.root, &settings);
|
||||
}
|
||||
if config.command == ide_sdk::godot::COMMAND {
|
||||
return Err(IdeError::unavailable(ide_sdk::godot::NOT_FOUND));
|
||||
}
|
||||
|
||||
@@ -51,3 +51,29 @@ async fn removed_detections_stay_removed_and_overrides_restore() {
|
||||
ops::run::save_configs(&host, "main", &[], &[]).unwrap();
|
||||
assert!(!ops::run::configs(&host, "main").unwrap().iter().any(|c| c.id == id));
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn gradle_defaults_are_not_saved_as_explicit_run_choices() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let root = dunce::canonicalize(tmp.path()).unwrap();
|
||||
std::fs::write(root.join("build.gradle"), "plugins { id 'java' }").unwrap();
|
||||
let host = host(&root.join("app"));
|
||||
host.open_project("main", &root).unwrap();
|
||||
ops::project::set_settings(
|
||||
&host,
|
||||
"main",
|
||||
ide_workspace::settings::SettingsScope::Project,
|
||||
"languages.java.gradle.jvm",
|
||||
"\"/configured-jdk\"",
|
||||
)
|
||||
.unwrap();
|
||||
let configs = ops::run::configs(&host, "main").unwrap();
|
||||
let mut gradle = configs.into_iter().find(|c| c.kind == "gradle").unwrap();
|
||||
assert!(!gradle.env.contains_key("JAVA_HOME"));
|
||||
gradle.detected = false;
|
||||
gradle.env.insert("JAVA_HOME".into(), "/explicit-jdk".into());
|
||||
ops::run::save_configs(&host, "main", &[gradle.clone()], &[]).unwrap();
|
||||
let saved = ops::run::configs(&host, "main").unwrap().into_iter().find(|c| c.id == gradle.id).unwrap();
|
||||
assert_eq!(saved.env["JAVA_HOME"], "/explicit-jdk");
|
||||
assert_eq!(saved.command, gradle.command);
|
||||
}
|
||||
@@ -116,6 +116,43 @@ pub fn maven_command(root: &Path, s: &Settings) -> Option<PathBuf> {
|
||||
found
|
||||
}
|
||||
|
||||
/// Project launcher policy for sync and tool-window tasks.
|
||||
pub fn gradle_command(root: &Path, build_dir: &Path, s: &Settings) -> PathBuf {
|
||||
resolve_gradle_command(root, build_dir, s, true)
|
||||
}
|
||||
|
||||
fn resolve_gradle_command(root: &Path, build_dir: &Path, s: &Settings, use_wrapper: bool) -> PathBuf {
|
||||
let g = &s.languages.java.gradle;
|
||||
let name = if cfg!(windows) { "gradle.bat" } else { "gradle" };
|
||||
if g.distribution == ide_core::settings::GradleDistribution::Local
|
||||
&& let Some(home) = setting_path(root, &g.home)
|
||||
{
|
||||
return home.join("bin").join(name);
|
||||
}
|
||||
let wrapper = build_dir.join(if cfg!(windows) { "gradlew.bat" } else { "gradlew" });
|
||||
if use_wrapper && wrapper.is_file() {
|
||||
return wrapper;
|
||||
}
|
||||
pinned::pinned_install(build_dir, Tool::Gradle)
|
||||
.map(|(home, _)| home.join("bin").join(name))
|
||||
.unwrap_or_else(|| name.into())
|
||||
}
|
||||
|
||||
/// Gradle JVM overrides Project JDK; model callers supply the module's fallback environment.
|
||||
pub fn gradle_env(
|
||||
root: &Path,
|
||||
s: &Settings,
|
||||
mut env: std::collections::BTreeMap<String, String>,
|
||||
) -> std::collections::BTreeMap<String, String> {
|
||||
if let Some(home) = setting_path(root, &s.languages.java.gradle.jvm) {
|
||||
env.insert("JAVA_HOME".into(), home.to_string_lossy().into_owned());
|
||||
}
|
||||
if let Some(home) = setting_path(root, &s.languages.java.gradle.user_home) {
|
||||
env.insert("GRADLE_USER_HOME".into(), home.to_string_lossy().into_owned());
|
||||
}
|
||||
env
|
||||
}
|
||||
|
||||
/// Maven options from settings, placed before the goals: `-s <settings.xml>`
|
||||
/// and `-Dmaven.repo.local=<dir>`.
|
||||
pub fn maven_options(root: &Path, s: &Settings) -> Vec<String> {
|
||||
@@ -532,7 +569,6 @@ pub fn apply_to_run_configs(configs: &mut [RunConfig], root: &Path, s: &Settings
|
||||
let jdk = project_jdk(root, s);
|
||||
let mvn = maven_command(root, s);
|
||||
let mvn_options = maven_options(root, s);
|
||||
let gradle = pinned::pinned_install(root, Tool::Gradle).map(|(dir, _)| dir.join("bin"));
|
||||
|
||||
for c in configs.iter_mut() {
|
||||
resolve_run_command(c, root, s);
|
||||
@@ -600,15 +636,25 @@ pub fn apply_to_run_configs(configs: &mut [RunConfig], root: &Path, s: &Settings
|
||||
c.env.entry("GOPATH".into()).or_insert_with(|| gopath.to_string_lossy().into_owned());
|
||||
}
|
||||
}
|
||||
"maven" | "gradle" => {
|
||||
"gradle" => {
|
||||
let build_dir = c.cwd.as_ref().map(|p| root.join(p.as_ref())).unwrap_or_else(|| root.to_path_buf());
|
||||
if matches!(c.command.as_str(), "gradle" | "gradle.bat") {
|
||||
c.command = resolve_gradle_command(root, &build_dir, s, false).to_string_lossy().into_owned();
|
||||
}
|
||||
let defaults = jdk
|
||||
.as_ref()
|
||||
.map(|j| {
|
||||
std::collections::BTreeMap::from([("JAVA_HOME".into(), j.home.to_string_lossy().into_owned())])
|
||||
})
|
||||
.unwrap_or_default();
|
||||
for (key, value) in gradle_env(root, s, defaults) {
|
||||
c.env.entry(key).or_insert(value);
|
||||
}
|
||||
}
|
||||
"maven" => {
|
||||
if let Some(jdk) = &jdk {
|
||||
c.env.entry("JAVA_HOME".into()).or_insert_with(|| jdk.home.to_string_lossy().into_owned());
|
||||
}
|
||||
if c.kind == "gradle"
|
||||
&& let Some(bin) = gradle.as_ref().filter(|b| b.is_dir())
|
||||
{
|
||||
path_dirs.push(bin.clone());
|
||||
}
|
||||
if c.kind == "maven" && c.detected {
|
||||
if let Some(mvn) = &mvn {
|
||||
c.command = mvn.to_string_lossy().into_owned();
|
||||
|
||||
@@ -698,3 +698,58 @@ fn max_java_picks_an_older_jdk() {
|
||||
crate::java::cap_java_home(&mut free, &jdks);
|
||||
assert_eq!(free.env["JAVA_HOME"], j25.home.to_string_lossy());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gradle_settings_precedence() {
|
||||
use crate::apply::{gradle_command, gradle_env};
|
||||
use ide_core::settings::GradleDistribution;
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let root = tmp.path();
|
||||
let plain = if cfg!(windows) { "gradle.bat" } else { "gradle" };
|
||||
let wrapper = root.join(if cfg!(windows) { "gradlew.bat" } else { "gradlew" });
|
||||
let mut settings = Settings::default();
|
||||
assert_eq!(gradle_command(root, root, &settings), std::path::PathBuf::from(plain));
|
||||
std::fs::write(&wrapper, "").unwrap();
|
||||
assert_eq!(gradle_command(root, root, &settings), wrapper);
|
||||
let fallback = std::collections::BTreeMap::from([("JAVA_HOME".into(), "project-jdk".into())]);
|
||||
assert_eq!(gradle_env(root, &settings, fallback.clone()), fallback);
|
||||
settings.languages.java.gradle.jvm = "gradle-jdk".into();
|
||||
settings.languages.java.gradle.user_home = "gradle-cache".into();
|
||||
settings.languages.java.gradle.home = "local-gradle".into();
|
||||
let env = gradle_env(root, &settings, fallback);
|
||||
assert_eq!(env["JAVA_HOME"], root.join("gradle-jdk").to_string_lossy());
|
||||
assert_eq!(env["GRADLE_USER_HOME"], root.join("gradle-cache").to_string_lossy());
|
||||
// A stored local home is inactive in wrapper mode.
|
||||
assert_eq!(gradle_command(root, root, &settings), wrapper);
|
||||
for local in [false, true] {
|
||||
settings.languages.java.gradle.distribution =
|
||||
if local { GradleDistribution::Local } else { GradleDistribution::Wrapper };
|
||||
let expected = if local { root.join("local-gradle/bin").join(plain) } else { plain.into() };
|
||||
for explicit_java in [false, true] {
|
||||
for is_test in [false, true] {
|
||||
let mut configs =
|
||||
[run("gradle", wrapper.to_str().unwrap(), &["test"]), run("gradle", plain, &["test"])];
|
||||
for c in &mut configs {
|
||||
c.is_test = is_test;
|
||||
if explicit_java {
|
||||
c.env.insert("JAVA_HOME".into(), "explicit-jdk".into());
|
||||
}
|
||||
}
|
||||
apply_to_run_configs(&mut configs, root, &settings);
|
||||
assert_eq!(configs[0].command, wrapper.to_string_lossy());
|
||||
assert_eq!(configs[1].command, expected.to_string_lossy());
|
||||
for c in &configs {
|
||||
assert_eq!(
|
||||
c.env["JAVA_HOME"],
|
||||
if explicit_java {
|
||||
"explicit-jdk".into()
|
||||
} else {
|
||||
root.join("gradle-jdk").to_string_lossy().into_owned()
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
assert_eq!(gradle_command(root, root, &settings), if local { expected } else { wrapper.clone() });
|
||||
}
|
||||
}
|
||||
@@ -116,10 +116,21 @@ fn mise_and_asdf_installs_are_detected_and_pinned_versions_used() {
|
||||
assert_eq!(path_of(&configs[0])[0], installs.join("node/20.11.1/bin"));
|
||||
assert_eq!(configs[1].command, goroot.join("bin/go").to_string_lossy());
|
||||
assert_eq!(configs[1].env["GOROOT"], goroot.to_string_lossy());
|
||||
assert_eq!(path_of(&configs[2])[0], installs.join("gradle/8.7/bin"));
|
||||
assert_eq!(configs[2].command, installs.join("gradle/8.7/bin/gradle").to_string_lossy());
|
||||
assert_eq!(configs[2].env["JAVA_HOME"], jdk.home.to_string_lossy());
|
||||
assert_eq!(configs[3].command, maven.join("bin/mvn").to_string_lossy());
|
||||
|
||||
assert_eq!(ide_sdk::apply::gradle_command(&project, &project, &s), installs.join("gradle/8.7/bin/gradle"));
|
||||
script(&project.join("gradlew"), "");
|
||||
assert_eq!(ide_sdk::apply::gradle_command(&project, &project, &s), project.join("gradlew"));
|
||||
let mut local = s.clone();
|
||||
local.languages.java.gradle.distribution = ide_core::settings::GradleDistribution::Local;
|
||||
local.languages.java.gradle.home = "local-gradle".into();
|
||||
assert_eq!(ide_sdk::apply::gradle_command(&project, &project, &local), project.join("local-gradle/bin/gradle"));
|
||||
let mut plain = [run("gradle", "gradle")];
|
||||
ide_sdk::apply::apply_to_run_configs(&mut plain, &project, &s);
|
||||
assert_eq!(plain[0].command, installs.join("gradle/8.7/bin/gradle").to_string_lossy());
|
||||
|
||||
// The project's Maven wrapper keeps precedence over a pinned Maven.
|
||||
script(&project.join("mvnw"), "");
|
||||
assert_eq!(ide_sdk::apply::maven_command(&project, &s), None);
|
||||
|
||||
@@ -3139,6 +3139,8 @@ export type GoSettings = {
|
||||
|
||||
export type GotoKind = "definition" | "declaration" | "typeDefinition" | "implementation";
|
||||
|
||||
export type GradleDistribution = "wrapper" | "local";
|
||||
|
||||
export type GradleModel = {
|
||||
/** Project-relative directory of the build (where `settings.gradle` is); `""` = project root. */
|
||||
dir: string,
|
||||
@@ -3166,6 +3168,17 @@ export type GradleProject = {
|
||||
tasks: GradleTask[],
|
||||
};
|
||||
|
||||
/** Build Tools › Gradle. */
|
||||
export type GradleSettings = {
|
||||
/** Empty = Project JDK; otherwise a JDK home. */
|
||||
jvm?: string,
|
||||
distribution?: GradleDistribution,
|
||||
/** Used only for the local distribution. */
|
||||
home?: string,
|
||||
/** Empty = Gradle's default user home. */
|
||||
userHome?: string,
|
||||
};
|
||||
|
||||
export type GradleTask = {
|
||||
name: string,
|
||||
/** Lower-case group (`build`, `build setup`, `other`). */
|
||||
@@ -3614,6 +3627,8 @@ export type JavaSettings = {
|
||||
jdtlsVmArgs?: string,
|
||||
/** Build Tools › Maven. */
|
||||
maven?: MavenSettings,
|
||||
/** Build Tools › Gradle. */
|
||||
gradle?: GradleSettings,
|
||||
};
|
||||
|
||||
/** Java versions a vendor publishes. */
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useProject } from "@/platform/project";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { fireEvent, render, screen } from "@testing-library/react";
|
||||
import { registerActions } from "@/platform/actions";
|
||||
@@ -22,6 +23,7 @@ import { pasteRows } from "./EnvDialog";
|
||||
import { CURRENT_FILE, resetRuns, selectConfig, sessionsOf, startCompound, startRun, useRun } from "./store";
|
||||
import {
|
||||
BUILTIN_TYPES,
|
||||
blankTemplate,
|
||||
formParams,
|
||||
missingParams,
|
||||
newConfigOfType,
|
||||
@@ -519,3 +521,23 @@ describe("Current File", () => {
|
||||
expect(useRun.getState().selectedId).toBe(CURRENT_FILE);
|
||||
});
|
||||
});
|
||||
|
||||
describe("new Gradle wrapper default", () => {
|
||||
it.each(["wrapper", "local"] as const)(
|
||||
"follows %s distribution without changing existing choices",
|
||||
(distribution) => {
|
||||
const previous = useProject.getState().settings;
|
||||
useProject.setState({ settings: { languages: { java: { gradle: { distribution } } } } as never });
|
||||
try {
|
||||
const type = BUILTIN_TYPES.find((t) => t.id === "gradle")!;
|
||||
const config = newConfigOfType(type, [], blankTemplate(type));
|
||||
expect(config.params?.wrapper).toBe(distribution === "local" ? "false" : "true");
|
||||
expect(type.fields.some((f) => f.key === "wrapper")).toBe(true);
|
||||
const template = { ...config, params: { wrapper: "true" } };
|
||||
expect(newConfigOfType(type, [], template).params?.wrapper).toBe("true");
|
||||
} finally {
|
||||
useProject.setState({ settings: previous });
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
type ForkedJvm,
|
||||
} from "./jvmTypes";
|
||||
import { tx } from "@/platform/i18n";
|
||||
import { useProject } from "@/platform/project";
|
||||
|
||||
export const COMPOUND = "compound";
|
||||
|
||||
@@ -501,11 +502,19 @@ export function blankTemplate(type: RunConfigTypeContribution): RunConfig {
|
||||
isTest: false,
|
||||
debugAdapter: type.debugAdapter ?? null,
|
||||
},
|
||||
{ ...type.defaults },
|
||||
newConfigDefaults(type),
|
||||
type,
|
||||
);
|
||||
}
|
||||
|
||||
function newConfigDefaults(type: RunConfigTypeContribution): Record<string, string> {
|
||||
const defaults = { ...type.defaults };
|
||||
if (type.id === "gradle" && useProject.getState().settings?.languages?.java?.gradle?.distribution === "local") {
|
||||
defaults.wrapper = "false";
|
||||
}
|
||||
return defaults;
|
||||
}
|
||||
|
||||
/** A new configuration of `type`, with the type's defaults and then its template applied. */
|
||||
export function newConfigOfType(
|
||||
type: RunConfigTypeContribution,
|
||||
@@ -534,7 +543,7 @@ export function newConfigOfType(
|
||||
if (v !== undefined) (copied as Record<string, unknown>)[key] = structuredClone(v);
|
||||
}
|
||||
}
|
||||
const params = { ...type.defaults, ...(template?.params ?? {}) };
|
||||
const params = { ...newConfigDefaults(type), ...(template?.params ?? {}) };
|
||||
const next = withParams({ ...base, ...copied }, params, type);
|
||||
return type.debugAdapter ? { ...next, debugAdapter: type.debugAdapter } : next;
|
||||
}
|
||||
@@ -65,6 +65,28 @@ async function openDialog(page?: string) {
|
||||
}
|
||||
|
||||
describe("Settings dialog", () => {
|
||||
it("finds Gradle JVM and applies project Gradle settings", async () => {
|
||||
useProject.setState({ project: { root: "/project" } as never });
|
||||
const dialog = await openDialog("build.gradle");
|
||||
fireEvent.change(within(dialog).getByLabelText("Search settings"), { target: { value: "Gradle JVM" } });
|
||||
expect(within(dialog).getByRole("tree", { name: "Settings pages" }).textContent).toContain("Gradle");
|
||||
expect(within(dialog).getByLabelText("Gradle JVM")).toBeInTheDocument();
|
||||
await act(async () => stage("build.gradle", "project", "languages.java.gradle.jvm", "/jdk21"));
|
||||
await act(async () => stage("build.gradle", "project", "languages.java.gradle.distribution", "local"));
|
||||
expect(within(dialog).getByPlaceholderText("Gradle installation directory")).toBeInTheDocument();
|
||||
await act(async () => stage("build.gradle", "project", "languages.java.gradle.home", "/gradle"));
|
||||
await act(async () => stage("build.gradle", "project", "languages.java.gradle.userHome", "/gradle-cache"));
|
||||
await act(async () => fireEvent.click(within(dialog).getByRole("button", { name: "Apply" })));
|
||||
expect(settingsSet.mock.calls).toEqual(
|
||||
expect.arrayContaining([
|
||||
["project", "languages.java.gradle.jvm", '"/jdk21"'],
|
||||
["project", "languages.java.gradle.distribution", '"local"'],
|
||||
["project", "languages.java.gradle.home", '"/gradle"'],
|
||||
["project", "languages.java.gradle.userHome", '"/gradle-cache"'],
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it("searches pages and highlights matching option labels", async () => {
|
||||
const dialog = await openDialog("appearance");
|
||||
fireEvent.change(within(dialog).getByLabelText("Search settings"), { target: { value: "soft wrap" } });
|
||||
|
||||
@@ -266,7 +266,57 @@ export function JavaPage() {
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Build Tools › Maven
|
||||
// Build Tools › Gradle / Maven
|
||||
|
||||
export function GradlePage() {
|
||||
const [distribution, setDistribution] = useSetting<string>("languages.java.gradle.distribution");
|
||||
return (
|
||||
<div className={styles.page}>
|
||||
<Section>
|
||||
<SdkRow
|
||||
k="languages.java.gradle.jvm"
|
||||
label="Gradle JVM"
|
||||
kind="jdk"
|
||||
emptyLabel="Project JDK"
|
||||
directory
|
||||
browseTitle="Select JDK Home Directory"
|
||||
aria-label="Gradle JVM"
|
||||
hint="JDK used for Gradle sync, tasks, run configurations and tests. An explicit JAVA_HOME in a run configuration takes precedence."
|
||||
/>
|
||||
<Row
|
||||
label="Distribution"
|
||||
hint="Uses the Gradle wrapper when present, otherwise the project-pinned mise/asdf Gradle, then Gradle on PATH."
|
||||
>
|
||||
<Select
|
||||
value={distribution}
|
||||
onChange={setDistribution}
|
||||
aria-label="Distribution"
|
||||
options={[
|
||||
{ value: "wrapper", label: "Gradle wrapper" },
|
||||
{ value: "local", label: "Local installation" },
|
||||
]}
|
||||
/>
|
||||
</Row>
|
||||
{distribution === "local" && (
|
||||
<PathInput
|
||||
k="languages.java.gradle.home"
|
||||
label="Gradle home"
|
||||
placeholder="Gradle installation directory"
|
||||
title="Select Gradle Home Directory"
|
||||
hint="Uses bin/gradle (bin/gradle.bat on Windows) instead of the wrapper or pinned toolchain."
|
||||
/>
|
||||
)}
|
||||
<PathInput
|
||||
k="languages.java.gradle.userHome"
|
||||
label="Gradle user home"
|
||||
placeholder="~/.gradle"
|
||||
title="Select Gradle User Home Directory"
|
||||
hint="Sets GRADLE_USER_HOME. Empty uses Gradle's default."
|
||||
/>
|
||||
</Section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function MavenPage() {
|
||||
return (
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
/**
|
||||
* Build, Execution, Deployment pages: Maven, Console, Debugger (+ Data Views,
|
||||
* Build, Execution, Deployment pages: Gradle, Maven, Console, Debugger (+ Data Views,
|
||||
* Stepping), Python Debugger and Run/Debug. Consumed by the run and debugger
|
||||
* features (and `ide-run` for encoding / termination timeout).
|
||||
*/
|
||||
import { WORKFLOW_LIMITS } from "@/platform/workflowSettings";
|
||||
import type { SelectOption } from "@/ui";
|
||||
import { GlobListEditor, NumberInput, Row } from "../components/Form";
|
||||
import { MavenPage } from "../languages/LanguagePages";
|
||||
import { GradlePage, MavenPage } from "../languages/LanguagePages";
|
||||
import { useSetting } from "../components/context";
|
||||
import type { PageSpec } from "./spec";
|
||||
import { Indented, ToggleNumber } from "./workflowControls";
|
||||
@@ -90,6 +90,29 @@ function TooltipDelayRow() {
|
||||
}
|
||||
|
||||
export const EXECUTION_PAGES: PageSpec[] = [
|
||||
{
|
||||
id: "build.gradle",
|
||||
projectScope: true,
|
||||
defaultScope: "project",
|
||||
title: "Gradle",
|
||||
group: BUILD_GROUP,
|
||||
order: 358,
|
||||
keywords: [
|
||||
"Gradle JVM",
|
||||
"JDK",
|
||||
"JAVA_HOME",
|
||||
"distribution",
|
||||
"wrapper",
|
||||
"gradlew",
|
||||
"local installation",
|
||||
"Gradle home",
|
||||
"Gradle user home",
|
||||
"GRADLE_USER_HOME",
|
||||
"mise",
|
||||
"asdf",
|
||||
],
|
||||
component: GradlePage,
|
||||
},
|
||||
{
|
||||
id: "build.maven",
|
||||
projectScope: true,
|
||||
|
||||
@@ -21,6 +21,7 @@ export interface LanguagesValid {
|
||||
jdk: string;
|
||||
jdtlsJdk: string;
|
||||
jdtlsVmArgs: string;
|
||||
gradle: { jvm: string; distribution: "wrapper" | "local"; home: string; userHome: string };
|
||||
maven: { home: string; userSettings: string; localRepository: string; unlinkedProjects: string[] };
|
||||
};
|
||||
}
|
||||
@@ -58,6 +59,7 @@ export const DEFAULT_LANGUAGES: LanguagesValid = {
|
||||
jdk: "",
|
||||
jdtlsJdk: "",
|
||||
jdtlsVmArgs: "",
|
||||
gradle: { jvm: "", distribution: "wrapper", home: "", userHome: "" },
|
||||
maven: { home: "", userSettings: "", localRepository: "", unlinkedProjects: [] },
|
||||
},
|
||||
};
|
||||
@@ -115,6 +117,7 @@ export function validateLanguages(raw: unknown): LanguagesValid {
|
||||
const ts = obj(l.typescript);
|
||||
const java = obj(l.java);
|
||||
const maven = obj(java.maven);
|
||||
const gradle = obj(java.gradle);
|
||||
return {
|
||||
python: {
|
||||
interpreter: text(py.interpreter, d.python.interpreter),
|
||||
@@ -137,6 +140,12 @@ export function validateLanguages(raw: unknown): LanguagesValid {
|
||||
jdk: text(java.jdk, d.java.jdk),
|
||||
jdtlsJdk: text(java.jdtlsJdk, d.java.jdtlsJdk),
|
||||
jdtlsVmArgs: text(java.jdtlsVmArgs, d.java.jdtlsVmArgs),
|
||||
gradle: {
|
||||
jvm: text(gradle.jvm, d.java.gradle.jvm),
|
||||
distribution: oneOf(gradle.distribution, ["wrapper", "local"] as const, d.java.gradle.distribution),
|
||||
home: text(gradle.home, d.java.gradle.home),
|
||||
userHome: text(gradle.userHome, d.java.gradle.userHome),
|
||||
},
|
||||
maven: {
|
||||
home: text(maven.home, d.java.maven.home),
|
||||
userSettings: text(maven.userSettings, d.java.maven.userSettings),
|
||||
|
||||
@@ -4816,5 +4816,18 @@
|
||||
"busy": "忙碌",
|
||||
"dead": "已终止",
|
||||
"idle": "空闲",
|
||||
"starting": "正在启动"
|
||||
"starting": "正在启动",
|
||||
"Gradle JVM": "Gradle 使用的 JVM",
|
||||
"Distribution": "分发",
|
||||
"Gradle wrapper": "使用 Gradle 包装器",
|
||||
"Local installation": "本地安装",
|
||||
"Gradle home": "Gradle 主路径",
|
||||
"Gradle user home": "Gradle 用户主目录",
|
||||
"Gradle installation directory": "Gradle 安装目录",
|
||||
"Select Gradle Home Directory": "选择 Gradle 主目录",
|
||||
"Select Gradle User Home Directory": "选择 Gradle 用户主目录",
|
||||
"JDK used for Gradle sync, tasks, run configurations and tests. An explicit JAVA_HOME in a run configuration takes precedence.": "用于 Gradle 同步、任务、运行配置和测试的 JDK。运行配置中显式设置的 JAVA_HOME 优先。",
|
||||
"Uses the Gradle wrapper when present, otherwise the project-pinned mise/asdf Gradle, then Gradle on PATH.": "优先使用 Gradle 包装器,否则使用项目通过 mise/asdf 固定的 Gradle,最后使用 PATH 中的 Gradle。",
|
||||
"Uses bin/gradle (bin/gradle.bat on Windows) instead of the wrapper or pinned toolchain.": "使用 bin/gradle(Windows 上为 bin/gradle.bat),覆盖包装器或固定的工具链。",
|
||||
"Sets GRADLE_USER_HOME. Empty uses Gradle's default.": "设置 GRADLE_USER_HOME。留空则使用 Gradle 默认值。"
|
||||
}
|
||||
Reference in new issue
Заблокувати користувача