feat(build): add Maven thread count setting (#1800)

Signed-off-by: Inf1mum <[email protected]>
This commit is contained in:
Vadim Lazuk authored and GitHub committed 2026-11-07 02:49:38 +08:00
1 parent ebac061a1f
commit b5974ab3ac
10 files changed
+183 -9

No files matched your search

+32 -1
View File
@@ -95,6 +95,24 @@ fn maven_sync_goals(effective: &Path, offline: bool) -> Vec<String> {
"-Dverbose".into(),
]
}
/// The settings' Maven options for the sync runs: the thread count (`-T`, also
/// `--threads`) is left out, because the sync pins its own `-T1` — its console
/// output is parsed as one stream, which a parallel build interleaves.
fn sync_maven_options(options: &[String]) -> Vec<String> {
let mut out = Vec::with_capacity(options.len());
let mut args = options.iter();
while let Some(arg) = args.next() {
if arg == "-T" || arg == "--threads" {
args.next(); // the count itself
} else if (arg.starts_with("-T") && arg.len() > 2) || arg.starts_with("--threads=") {
// `--threads=4` / `-T4`: the count is part of the argument.
} else {
out.push(arg.clone());
}
}
out
}
const GRADLE_CACHE: &str = "gradle.json";
const MAVEN_PROFILES: &str = "maven-profiles.json";
@@ -324,7 +342,7 @@ pub fn sync_maven(
// `dependency:tree` output of concurrent modules, which the sync parses
// as one stream. mvn is serial anyway, so this only pins the daemon.
let mut args: Vec<String> = vec!["-B".into(), "-fae".into(), "-Dstyle.color=never".into(), "-T1".into()];
args.extend(options.maven_options.iter().cloned());
args.extend(sync_maven_options(&options.maven_options));
if options.offline {
args.push("-o".into());
}
@@ -831,6 +849,19 @@ mod tests {
assert_eq!(error_summary("one\nlast line\n\n"), "last line");
}
#[test]
fn maven_sync_leaves_out_the_settings_thread_count() {
// The sync pins its own `-T1`; the user's `-T` (Settings › Maven) would win
// over it on the mvnd command line and interleave the parsed output.
let options = ["-s".to_owned(), "settings.xml".to_owned(), "-T".to_owned(), "4".to_owned()];
assert_eq!(sync_maven_options(&options), ["-s", "settings.xml"]);
let options = ["--threads".to_owned(), "2C".to_owned(), "-Dmaven.repo.local=/m2".to_owned()];
assert_eq!(sync_maven_options(&options), ["-Dmaven.repo.local=/m2"]);
assert_eq!(sync_maven_options(&["-T4".to_owned()]), Vec::<String>::new());
assert_eq!(sync_maven_options(&["--threads=4".to_owned()]), Vec::<String>::new());
assert!(sync_maven_options(&[]).is_empty());
}
#[cfg(unix)]
#[test]
fn maven_sync_runs_the_daemon_command_serially() {
+53
View File
@@ -919,6 +919,24 @@ pub struct MavenSettings {
/// Maven Daemon home (or its `mvnd` executable); empty = `$MVND_HOME`, else
/// `mvnd` on `PATH` / common install dirs. Only used when `daemon` is on.
pub mvnd_home: String,
/// Thread count of Maven builds (`-T`): a number of threads (`4`) or a core
/// multiplier (`2C`, `2.5C`); empty = Maven's default (one thread per module).
pub threads: String,
}
impl MavenSettings {
/// The `-T` value when it is one Maven accepts: a thread count (`4`) or a
/// core multiplier with an optional decimal fraction (`1C`, `2.5C`).
/// Anything else is dropped: a bad `-T` would make every Maven run fail.
pub fn thread_count(&self) -> Option<&str> {
let value = self.threads.trim();
let number = value.strip_suffix('C').unwrap_or(value);
let mut decimal = number.split('.');
let well_formed = number.parse::<f64>().is_ok_and(|n| n > 0.0)
&& decimal.clone().count() <= 2
&& decimal.all(|part| !part.is_empty() && part.chars().all(|c| c.is_ascii_digit()));
well_formed.then_some(value)
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Type)]
@@ -1161,6 +1179,41 @@ mod maven_linkage_tests {
assert!(encoded.contains("\"daemon\":true") && encoded.contains("mvndHome"));
assert_eq!(serde_json::from_str::<MavenSettings>(&encoded).unwrap(), settings);
}
#[test]
fn thread_count_takes_counts_and_core_multipliers_only() {
let threads = |t: &str| {
let m = MavenSettings { threads: t.into(), ..Default::default() };
m.thread_count().map(str::to_owned)
};
// Empty = Maven's default (one thread per module).
assert_eq!(threads("").as_deref(), None);
assert_eq!(threads(" ").as_deref(), None);
// A thread count, and a core multiplier whose fraction may be decimal.
assert_eq!(threads("4").as_deref(), Some("4"));
assert_eq!(threads(" 2 ").as_deref(), Some("2"));
assert_eq!(threads("1C").as_deref(), Some("1C"));
assert_eq!(threads("2.0C").as_deref(), Some("2.0C"));
assert_eq!(threads("0.5C").as_deref(), Some("0.5C"));
// Everything else a `-T` would make every Maven run reject is dropped.
assert_eq!(threads("abc").as_deref(), None);
assert_eq!(threads("4x").as_deref(), None);
assert_eq!(threads("0").as_deref(), None);
assert_eq!(threads("-4").as_deref(), None);
assert_eq!(threads("1e3").as_deref(), None);
assert_eq!(threads("2..5C").as_deref(), None);
}
#[test]
fn thread_count_round_trips_and_defaults_to_empty() {
// Old settings files predate the thread count; `serde(default)` keeps them loadable.
let old: MavenSettings = serde_json::from_str(r#"{"home":"/opt/maven"}"#).unwrap();
assert!(old.threads.is_empty() && old.thread_count().is_none());
let settings = MavenSettings { threads: "2C".into(), ..old };
let encoded = serde_json::to_string(&settings).unwrap();
assert!(encoded.contains("\"threads\":\"2C\""));
assert_eq!(serde_json::from_str::<MavenSettings>(&encoded).unwrap(), settings);
}
}
#[cfg(test)]
+15 -6
View File
@@ -370,8 +370,8 @@ pub fn gradle_env(
env
}
/// Maven options from settings, placed before the goals: `-s <settings.xml>`
/// and `-Dmaven.repo.local=<dir>`.
/// Maven options from settings, placed before the goals: `-s <settings.xml>`,
/// `-Dmaven.repo.local=<dir>` and `-T <count>` (the thread count).
pub fn maven_options(root: &Path, s: &Settings) -> Vec<String> {
let m = &s.languages.java.maven;
let mut out = Vec::new();
@@ -381,6 +381,9 @@ pub fn maven_options(root: &Path, s: &Settings) -> Vec<String> {
if let Some(dir) = setting_path(root, &m.local_repository) {
out.push(format!("-Dmaven.repo.local={}", dir.to_string_lossy()));
}
if let Some(threads) = m.thread_count() {
out.extend(["-T".to_owned(), threads.to_owned()]);
}
out
}
@@ -388,7 +391,7 @@ fn maven_settings_prefixes(args: &[String]) -> Vec<usize> {
let mut prefixes = vec![0];
let mut i = 0;
while let Some(arg) = args.get(i) {
if arg == "-s" && args.get(i + 1).is_some() {
if (arg == "-s" || arg == "-T") && args.get(i + 1).is_some() {
i += 2;
} else if arg.starts_with("-Dmaven.repo.local=") {
i += 1;
@@ -1043,22 +1046,28 @@ fn is_python_command(cmd: &str) -> bool {
const PY_RUNNERS: [&str; 3] = ["uv", "poetry", "pdm"];
/// [`maven_options`] as the groups they are passed in (`-s <file>`, `-Dmaven.repo.local=…`).
/// [`maven_options`] as the groups they are passed in (`-s <file>`, `-Dmaven.repo.local=…`,
/// `-T <count>`).
fn maven_option_groups(options: &[String]) -> Vec<&[String]> {
let mut groups = Vec::new();
let mut i = 0;
while i < options.len() {
let n = if options[i] == "-s" { 2.min(options.len() - i) } else { 1 };
let n = if options[i] == "-s" || options[i] == "-T" { 2.min(options.len() - i) } else { 1 };
groups.push(&options[i..i + n]);
i += n;
}
groups
}
/// Whether `args` set what `group` sets: a user settings file, or the local repository.
/// Whether `args` set what `group` sets: a user settings file, the local
/// repository, or the thread count.
fn sets_maven_option(args: &[String], group: &[String]) -> bool {
match group.first().map(String::as_str) {
Some("-s") => args.iter().any(|a| a == "-s" || a == "--settings" || a.starts_with("--settings=")),
// `-T 4`, `--threads 4`, `-T4` and `--threads=4` all set the thread count.
Some("-T") => args.iter().any(|a| {
a == "-T" || a == "--threads" || a.starts_with("--threads=") || (a.starts_with("-T") && a.len() > 2)
}),
Some(flag) => {
let key = flag.split_once('=').map_or(flag, |(k, _)| k);
args.iter().any(|a| a.split_once('=').map_or(a.as_str(), |(k, _)| k) == key)
+35
View File
@@ -292,6 +292,41 @@ fn project_jdk_and_maven_settings() {
assert_eq!(project_jdk(tmp.path(), &s), None);
}
#[test]
fn maven_options_carry_the_thread_count_into_the_command_line() {
let tmp = tempfile::tempdir().unwrap();
let mut s = Settings::default();
assert!(maven_options(tmp.path(), &s).is_empty(), "empty settings add nothing");
s.languages.java.maven.user_settings = "m2/settings.xml".into();
s.languages.java.maven.local_repository = "m2/repo".into();
s.languages.java.maven.threads = "4".into();
let settings_file = tmp.path().join("m2/settings.xml").to_string_lossy().into_owned();
let repo = tmp.path().join("m2/repo").to_string_lossy().into_owned();
assert_eq!(
maven_options(tmp.path(), &s),
["-s", settings_file.as_str(), &format!("-Dmaven.repo.local={repo}"), "-T", "4"]
);
// A core multiplier passes through; anything Maven would reject is dropped
// rather than failing every build.
s.languages.java.maven.threads = "2.5C".into();
assert!(maven_options(tmp.path(), &s).ends_with(&["-T".to_owned(), "2.5C".to_owned()]));
s.languages.java.maven.threads = "abc".into();
assert!(!maven_options(tmp.path(), &s).iter().any(|a| a == "-T"));
// Run configurations get the options ahead of the goals, once per launch.
s.languages.java.maven.threads = "2C".into();
let mut configs = vec![run("maven", "./mvnw", &["package"]), run("maven", "./mvnw", &["-T", "8", "package"])];
apply_to_run_configs(&mut configs, tmp.path(), &s);
let args: Vec<String> = configs[0].args.clone();
assert_eq!(args[args.len() - 3..].join(" "), "-T 2C package", "the settings' thread count leads the goals");
let args: Vec<String> = configs[1].args.clone();
assert_eq!(args[args.len() - 3..].join(" "), "-T 8 package", "the configuration's own -T wins");
apply_to_run_configs(&mut configs, tmp.path(), &s);
assert_eq!(configs[0].args.iter().filter(|a| a.as_str() == "-T").count(), 1, "applied once");
}
#[test]
fn saved_maven_goal_configs_take_the_configured_launcher() {
if cfg!(windows) {
+5
View File
@@ -5349,6 +5349,11 @@ export type MavenSettings = {
* `mvnd` on `PATH` / common install dirs. Only used when `daemon` is on.
*/
mvndHome?: string,
/**
* Thread count of Maven builds (`-T`): a number of threads (`4`) or a core
* multiplier (`2C`, `2.5C`); empty = Maven's default (one thread per module).
*/
threads?: string,
};
export type MemoryCategory =
@@ -26,7 +26,7 @@ describe("languages settings", () => {
jdtlsVmArgs: " -Xmx2g ",
jdk: 21,
jdtlsJdk: " /opt/jdk-25 ",
maven: { home: " /opt/maven ", daemon: "yes", mvndHome: " /opt/mvnd " },
maven: { home: " /opt/maven ", daemon: "yes", mvndHome: " /opt/mvnd ", threads: " 2C " },
},
});
expect(v.python).toEqual({ interpreter: "/usr/bin/python3", activateVirtualenv: true });
@@ -45,8 +45,10 @@ describe("languages settings", () => {
linkedProjects: [],
daemon: false,
mvndHome: "/opt/mvnd",
threads: "2C",
});
expect(validateSettings({}).languages).toEqual(DEFAULT_LANGUAGES);
expect(validateLanguages({ java: { maven: { threads: 4 } } }).java.maven.threads).toBe("");
});
it("keeps the Ruby interpreter through settings normalization and defaults to Auto", () => {
@@ -477,6 +477,8 @@ export function GradlePage() {
export function MavenPage() {
const [daemon, setDaemon] = useSetting<boolean>("languages.java.maven.daemon");
const [threads, setThreads] = useSetting<string>("languages.java.maven.threads");
const id = useId();
return (
<div className={styles.page}>
<Section>
@@ -506,6 +508,23 @@ export function MavenPage() {
title="Select Local Repository"
hint="Passed as -Dmaven.repo.local."
/>
<Row
label="Thread count"
htmlFor={id}
hint="Passed as -T to Maven builds and run configurations: a thread count (4) or a core multiplier (2C, 2.5C) for parallel builds of multi-module projects. Empty runs Maven serially."
>
<span className={styles.pathRow}>
<Input
id={id}
mono
className={styles.pathInput}
value={threads}
placeholder="4, 2C"
onChange={(e) => setThreads(e.target.value.trim())}
aria-label="Thread count"
/>
</span>
</Row>
<Toggle
label="Use Maven Daemon (mvnd)"
checked={daemon}
+14 -1
View File
@@ -144,7 +144,20 @@ export const EXECUTION_PAGES: PageSpec[] = [
group: BUILD_GROUP,
// IntelliJ IDEA: Build Tools › Maven, before Console / Debugger.
order: 359,
keywords: ["maven home", "mvn", "mvnw", "wrapper", "settings.xml", "user settings", "local repository", "m2"],
keywords: [
"maven home",
"mvn",
"mvnw",
"wrapper",
"settings.xml",
"user settings",
"local repository",
"m2",
"thread count",
"threads",
"-T",
"parallel",
],
component: MavenPage,
},
{
+4
View File
@@ -128,6 +128,8 @@ export interface LanguagesValid {
linkedProjects: string[];
daemon: boolean;
mvndHome: string;
/** `-T`: a thread count (4) or a core multiplier (2C, 2.5C); empty = Maven's default. */
threads: string;
};
};
/** Preview font size in px; `null` follows the editor font size. */
@@ -181,6 +183,7 @@ export const DEFAULT_LANGUAGES: LanguagesValid = {
linkedProjects: [],
daemon: false,
mvndHome: "",
threads: "",
},
},
markdown: { previewFontSize: null },
@@ -331,6 +334,7 @@ export function validateLanguages(raw: unknown): LanguagesValid {
linkedProjects: folders(maven.linkedProjects),
daemon: bool(maven.daemon, d.java.maven.daemon),
mvndHome: text(maven.mvndHome, d.java.maven.mvndHome),
threads: text(maven.threads, d.java.maven.threads),
},
},
markdown: { previewFontSize: previewSize(markdown.previewFontSize) },
+3
View File
@@ -1272,6 +1272,9 @@
"Maven sync finished with errors": "Maven 同步完成,但有错误",
"Maven wrapper, else mvn on PATH": "Maven wrapper,否则使用 PATH 中的 mvn",
"MVND_HOME, else auto-detected": "MVND_HOME,否则自动检测",
"Passed as -T to Maven builds and run configurations: a thread count (4) or a core multiplier (2C, 2.5C) for parallel builds of multi-module projects. Empty runs Maven serially.": "以 -T 传给 Maven 构建和运行配置:线程数(如 4)或内核倍数(如 2C、2.5C),用于多模块工程的并行构建。留空则串行运行 Maven。",
"Thread count": "线程数",
"4, 2C": "4、2C",
"Maximize": "最大化",
"Maximize Tool Window": "最大化工具窗口",
"Maximum local history size per project": "每个项目的最大本地历史记录大小",