fix(run): give Gradle JavaExec forks a UTF-8 default charset on Windows

Applications started by Gradle (bootRun, run, custom JavaExec tasks) kept the
ANSI default charset (GBK on Chinese Windows, JDK 8-17) while the Run console
decodes UTF-8. Code that converts text with the default charset then breaks:
Nacos config with Chinese text fails to parse (MalformedInputException) and
logback/log4j2 console output is garbled.

For a UTF-8 console on Windows, a Gradle launch now gets an init script that
adds -Dfile.encoding=UTF-8 to each JavaExec task when it runs, unless the task
or JAVA_TOOL_OPTIONS already sets file.encoding. Gradle's own JVM and compiler
source encoding are unchanged. Maven and Gradle are told apart so the
spring-boot:run rewrite only applies to Maven.
This commit is contained in:
l0ng-ai committed 2026-11-02 09:51:35 +08:00
1 parent 8ab0cba184
commit fc11eaf678
3 files changed
+95 -11

No files matched your search

+74 -6
View File
@@ -13,7 +13,10 @@
//! they fork get them too (`spring-boot:run`, Surefire, `bootRun`, test workers).
//! Each JVM then reports "Picked up JAVA_TOOL_OPTIONS: …" on stderr (which a build tool
//! may copy to its stdout); that line is hidden again ([`NoticeFilter`]). `file.encoding` is left alone there: it would change
//! how a build reads sources that declare no encoding.
//! how a build reads sources that declare no encoding. The application JVMs a build
//! forks get `file.encoding=UTF-8` for a UTF-8 console instead, through the build's own
//! options: `-Dspring-boot.run.jvmArguments` for Maven `spring-boot:run`, an init script
//! ([`GRADLE_FILE_ENCODING_SCRIPT`]) for Gradle `JavaExec` tasks (`bootRun`, `run`).
//! - Python: `PYTHONIOENCODING`.
use std::collections::BTreeMap;
@@ -78,15 +81,34 @@ const TOOL_OPTIONS: &str = "JAVA_TOOL_OPTIONS";
const SPRING_BOOT_JVM_ARGS: &str = "-Dspring-boot.run.jvmArguments=";
/// Build tools and launcher scripts that run on (and may fork) JVMs.
const JVM_TOOLS: &[&str] =
&["mvn", "mvnw", "mvnd", "gradle", "gradlew", "sbt", "ant", "kotlin", "kotlinc", "groovy", "scala", "jbang"];
const JVM_TOOLS: &[&str] = &["sbt", "ant", "kotlin", "kotlinc", "groovy", "scala", "jbang"];
const MAVEN: &[&str] = &["mvn", "mvnw", "mvnd"];
const GRADLE: &[&str] = &["gradle", "gradlew"];
/// Gradle init script giving the `JavaExec` tasks (`bootRun`, `run`, custom ones) a UTF-8
/// default charset, unless the task or the build already sets `file.encoding`. Checked when
/// the task runs, after the build script configured it; Gradle's own JVM and the compiler's
/// source encoding are untouched.
pub const GRADLE_FILE_ENCODING_SCRIPT: &str = "\
// Written by OpenIDEA: a UTF-8 default charset for applications run by Gradle, as the Run console decodes UTF-8.
allprojects {
tasks.withType(JavaExec).configureEach {
doFirst { task ->
if (!task.allJvmArgs.any { it.startsWith('-Dfile.encoding=') }) task.jvmArgs('-Dfile.encoding=UTF-8')
}
}
}
";
/// What a launch needs: `-D` options to put first on a `java` command line,
/// environment variables to set, and the notice line its JVMs print because of them.
#[derive(Debug, Default, PartialEq, Eq)]
pub struct EncodingFix {
pub java_args: Vec<String>,
/// The build tool's arguments, rewritten to pass options to the JVM it forks.
pub args: Option<Vec<String>>,
/// Gradle: pass [`GRADLE_FILE_ENCODING_SCRIPT`] with `--init-script`.
pub gradle_init_script: bool,
pub env: Vec<(String, String)>,
/// "Picked up JAVA_TOOL_OPTIONS: …" for a variable set here alone, to drop from the output.
pub notice: Option<String>,
@@ -118,6 +140,8 @@ fn options(properties: &[&str], encoding: &str) -> Vec<String> {
#[derive(Clone, Copy, PartialEq, Eq)]
enum Kind {
Java,
Maven,
Gradle,
JvmTool,
Python,
Other,
@@ -127,6 +151,10 @@ fn kind(program: &str) -> Kind {
let name = program_name(program);
if name == "java" || name == "javaw" {
Kind::Java
} else if MAVEN.contains(&name.as_str()) {
Kind::Maven
} else if GRADLE.contains(&name.as_str()) {
Kind::Gradle
} else if JVM_TOOLS.contains(&name.as_str()) {
Kind::JvmTool
} else if is_python(&name) {
@@ -195,12 +223,16 @@ fn fix_kind(
out.java_args.push(format!("-D{FILE_ENCODING}=UTF-8"));
}
}
(Kind::Java | Kind::JvmTool, _) => {
if let (Kind::JvmTool, Some(args)) = (kind, args)
(Kind::Java | Kind::Maven | Kind::Gradle | Kind::JvmTool, _) => {
if let Some(args) = args
&& is_utf8(encoding)
&& !sets(&tool_options, &[FILE_ENCODING])
{
out.args = spring_boot_file_encoding(args);
match kind {
Kind::Maven => out.args = spring_boot_file_encoding(args),
Kind::Gradle => out.gradle_init_script = true,
_ => {}
}
}
if !sets(&tool_options, STD_PROPERTIES) {
let added = options(STD_PROPERTIES, encoding).join(" ");
@@ -484,6 +516,42 @@ mod tests {
assert!(run("mvn", &["spring-boot:run"], &[], "UTF-8", false).args.is_none());
}
#[test]
fn forked_application_jvms_get_a_utf8_default_charset() {
// Reported: a Spring Boot app run by the build on Chinese Windows (JDK 8–17, GBK default).
for (tool, args) in [
("C:\\p\\gradlew.bat", &["bootRun"][..]),
("gradle", &[":app:run"]),
("mvnw.cmd", &["spring-boot:run"]),
("mvnd", &["org.springframework.boot:spring-boot-maven-plugin:2.7.18:run"]),
] {
let f = run(tool, args, &[], "UTF-8", true);
let gradle = tool.contains("gradle");
assert_eq!(f.gradle_init_script, gradle, "{tool}");
assert_eq!(f.args.is_some(), !gradle, "{tool}");
// The build tool's own JVM keeps its default charset.
assert!(!tool_env(&f).unwrap().contains("file.encoding"), "{tool}");
// The user's choice, a GBK console and other platforms are left alone.
let own = run(tool, args, &[(TOOL_OPTIONS, "-Dfile.encoding=GBK")], "UTF-8", true);
assert!(!own.gradle_init_script && own.args.is_none(), "{tool}");
let gbk = run(tool, args, &[], "GBK", true);
assert!(!gbk.gradle_init_script && gbk.args.is_none(), "{tool}");
assert_eq!(run(tool, args, &[], "UTF-8", false), EncodingFix::default(), "{tool}");
}
// Shell command lines can't take arguments; other JVM tools fork no application.
assert!(!fix_command_line("gradlew bootRun", &BTreeMap::new(), |_| None, "UTF-8", true).gradle_init_script);
let sbt = run("sbt", &["run"], &[], "UTF-8", true);
assert!(!sbt.gradle_init_script && sbt.args.is_none());
}
#[test]
fn gradle_script_respects_the_builds_own_file_encoding() {
let s = GRADLE_FILE_ENCODING_SCRIPT;
assert!(s.contains("tasks.withType(JavaExec).configureEach"), "{s}");
assert!(s.contains("allJvmArgs.any { it.startsWith('-Dfile.encoding=') }"), "{s}");
assert!(!s.contains("withType(Test)") && !s.contains("JavaCompile"), "{s}");
}
#[test]
fn shell_command_lines_get_variables_from_their_first_word() {
let line = |l: &str| fix_command_line(l, &BTreeMap::new(), |_| None, "UTF-8", true);
+8 -3
View File
@@ -41,17 +41,22 @@ pub fn default_dir() -> PathBuf {
/// Writes the script for `jvm_args` under `dir` (named by its content, so launches share
/// it and nothing needs deleting) and returns its path.
pub fn write_init_script(dir: &Path, jvm_args: &[String]) -> std::io::Result<PathBuf> {
write_script(dir, "jvm-args", &init_script(jvm_args))
}
/// Writes the init script `text` under `dir` as `<prefix>-<hash>.init.gradle` (written
/// once per content, so launches share it) and returns its path.
pub fn write_script(dir: &Path, prefix: &str, text: &str) -> std::io::Result<PathBuf> {
use std::hash::{Hash, Hasher};
let text = init_script(jvm_args);
let mut h = std::collections::hash_map::DefaultHasher::new();
text.hash(&mut h);
let file = dir.join(format!("jvm-args-{:016x}.init.gradle", h.finish()));
let file = dir.join(format!("{prefix}-{:016x}.init.gradle", h.finish()));
if std::fs::read_to_string(&file).is_ok_and(|t| t == text) {
return Ok(file);
}
std::fs::create_dir_all(dir)?;
let tmp = file.with_extension(format!("{}.tmp", std::process::id()));
std::fs::write(&tmp, &text)?;
std::fs::write(&tmp, text)?;
std::fs::rename(&tmp, &file).inspect_err(|_| {
let _ = std::fs::remove_file(&tmp);
})?;
+13 -2
View File
@@ -195,10 +195,21 @@ fn match_console_encoding(
output_encoding::fix(&program, &inv.args, &env.vars, inherited, encoding, cfg!(windows))
};
env.vars.extend(fix.env);
let changed = fix.args.is_some() || !fix.java_args.is_empty();
let mut prepend = fix.java_args;
if fix.gradle_init_script {
match crate::gradle_jvm::write_script(
&crate::gradle_jvm::default_dir(),
"file-encoding",
output_encoding::GRADLE_FILE_ENCODING_SCRIPT,
) {
Ok(script) => prepend.extend(["--init-script".to_owned(), script.to_string_lossy().into_owned()]),
Err(e) => tracing::warn!("cannot write the Gradle file.encoding init script: {e}"),
}
}
let changed = fix.args.is_some() || !prepend.is_empty();
if changed && !inv.raw_args {
inv.args = fix.args.unwrap_or_else(|| std::mem::take(&mut inv.args));
inv.args.splice(0..0, fix.java_args);
inv.args.splice(0..0, prepend);
let mut display = quote(config.command.trim());
for a in &inv.args {
display.push(' ');