11import { existsSync , readFileSync } from 'node:fs' ;
2- import { dirname , isAbsolute , join , resolve } from 'node:path' ;
2+ import { dirname , isAbsolute , join , relative , resolve } from 'node:path' ;
33import { parseStringPromise } from 'xml2js' ;
44import { CommandError , runCommand } from '../process/exec' ;
55import { assertJavaArguments } from '../process/java-arguments' ;
@@ -8,6 +8,8 @@ import type { BuildPlan, CommandSpec, MavenProject, PreparedProject, RunConfig }
88
99const HELP_PLUGIN = 'org.apache.maven.plugins:maven-help-plugin:3.5.1' ;
1010const DEPENDENCY_PLUGIN = 'org.apache.maven.plugins:maven-dependency-plugin:3.8.1' ;
11+ const windowsMavenVersions = new Map < string , Promise < void > > ( ) ;
12+ const baseDirectoryProperty = '-Dmaven.multiModuleProjectDirectory=${env.JAVA_RUN_MAVEN_BASE_DIRECTORY}' ;
1113
1214/** 优先采用项目 Wrapper,存在时不回退到其他 Maven 版本 */
1315function resolveMaven ( config : RunConfig ) : string {
@@ -25,7 +27,7 @@ function validateBuildArgs(args: string[]): void {
2527 if ( flags . has ( arg ) ) continue ;
2628 if ( arg . startsWith ( '-D' ) && arg . length > 2 ) {
2729 const name = arg . slice ( 2 ) . split ( '=' , 1 ) [ 0 ] ! ;
28- if ( ! [ 'output' , 'outputEncoding' , 'expression' , 'includeScope' , 'excludeScope' , 'skipTests' , 'maven.test.skip' , 'maven.main.skip' , 'maven.install.skip' ] . includes ( name )
30+ if ( ! [ 'output' , 'outputEncoding' , 'expression' , 'includeScope' , 'excludeScope' , 'skipTests' , 'maven.test.skip' , 'maven.main.skip' , 'maven.install.skip' , 'maven.multiModuleProjectDirectory' ] . includes ( name )
2931 && ! name . startsWith ( 'mdep.' ) && ! name . startsWith ( 'exec.' ) ) continue ;
3032 throw new Error ( `构建属性 ${ name } 由 java-run 管理,请使用对应的启动配置` ) ;
3133 }
@@ -49,6 +51,76 @@ function command(config: RunConfig, goals: string[], stage: string, alsoMake = f
4951 return { command : resolveMaven ( config ) , args, cwd : config . cwd , stage } ;
5052}
5153
54+ /**
55+ * 将 Maven 配置根转换为相对工作目录的路径
56+ *
57+ * @description 显式配置优先,否则采用最近的 .mvn 祖先;找不到时采用工作目录,跨盘路径保持绝对形式
58+ */
59+ export function resolveMavenBaseDirectory ( cwd : string , configuredBase ?: string ) : string {
60+ const directory = resolve ( cwd ) ;
61+ let base = configuredBase ? resolve ( directory , configuredBase ) : directory ;
62+ if ( ! configuredBase ) {
63+ while ( ! existsSync ( join ( base , '.mvn' ) ) ) {
64+ const parent = dirname ( base ) ;
65+ if ( parent === base ) {
66+ base = directory ;
67+ break ;
68+ }
69+ base = parent ;
70+ }
71+ }
72+ return relative ( directory , base ) || '.' ;
73+ }
74+
75+ async function executeMaven ( spec : CommandSpec ) : Promise < void > {
76+ let env : NodeJS . ProcessEnv | undefined ;
77+ let execution = spec ;
78+ let bridge = false ;
79+ const java = buildJava ( ) ;
80+ await assertJavaArguments ( java , spec . args , spec . cwd ) ;
81+ if ( process . platform === 'win32' ) {
82+ const configuredKey = Object . keys ( process . env ) . find ( name => name . toLowerCase ( ) === 'maven_basedir' ) ;
83+ const base = resolveMavenBaseDirectory ( spec . cwd , configuredKey ? process . env [ configuredKey ] : undefined ) ;
84+ const absoluteBase = resolve ( spec . cwd , base ) ;
85+ try {
86+ await assertJavaArguments ( java , [ `-Dmaven.multiModuleProjectDirectory=${ absoluteBase } ` ] , spec . cwd ) ;
87+ env = { MAVEN_BASEDIR : absoluteBase } ;
88+ } catch ( error ) {
89+ if ( ! ( error instanceof Error ) || ! error . message . includes ( '无法完整表示' ) ) throw error ;
90+ await assertJavaArguments ( java , [ `-Dmaven.multiModuleProjectDirectory=${ base } ` ] , spec . cwd ) ;
91+ // 初始化先用可表示的相对根,Maven CLI 再从 Unicode 子环境还原模型中的绝对根
92+ env = { MAVEN_BASEDIR : base , JAVA_RUN_MAVEN_BASE_DIRECTORY : absoluteBase } ;
93+ execution = { ...spec , args : [ ...spec . args , baseDirectoryProperty ] } ;
94+ bridge = true ;
95+ }
96+ }
97+ if ( bridge ) await validateWindowsMavenVersion ( spec , env ! ) ;
98+ console . error ( `java-run:${ spec . stage } ` ) ;
99+ const result = await runCommand ( execution , { capture : true , env } ) ;
100+ if ( result . exitCode !== 0 ) throw new CommandError ( execution , result ) ;
101+ }
102+
103+ async function validateWindowsMavenVersion ( spec : CommandSpec , env : NodeJS . ProcessEnv ) : Promise < void > {
104+ const key = `${ spec . command } \0${ spec . cwd } ` ;
105+ let checked = windowsMavenVersions . get ( key ) ;
106+ if ( ! checked ) {
107+ checked = ( async ( ) => {
108+ const versionCommand = { ...spec , args : [ '-B' , '-ntp' , '-version' ] , stage : '检测 Windows Maven 兼容性' } ;
109+ const result = await runCommand ( versionCommand , { capture : true , env } ) ;
110+ if ( result . exitCode !== 0 ) throw new CommandError ( versionCommand , result ) ;
111+ const output = `${ result . stdout } \n${ result . stderr } ` . replace ( / \x1b \[ [ 0 - 9 ; ] * m / g, '' ) ;
112+ const version = / A p a c h e M a v e n \s + ( \d + ) \. ( \d + ) \. ( \d + ) / . exec ( output ) ;
113+ if ( ! version ) throw new Error ( '无法确认 Maven 版本;Windows 需要 Maven 3.9.2 或更新版本以保留配置根的 Unicode 路径' ) ;
114+ const [ , major , minor , patch ] = version . map ( Number ) ;
115+ if ( major ! < 3 || ( major === 3 && ( minor ! < 9 || ( minor === 9 && patch ! < 2 ) ) ) ) {
116+ throw new Error ( `Windows 需要 Maven 3.9.2 或更新版本以保留配置根语义,实际版本为 ${ version [ 1 ] } .${ version [ 2 ] } .${ version [ 3 ] } ` ) ;
117+ }
118+ } ) ( ) ;
119+ windowsMavenVersions . set ( key , checked ) ;
120+ }
121+ await checked ;
122+ }
123+
52124/** 预览 Maven 的单目标准备与解析命令,不执行 Maven */
53125export function planMaven ( config : RunConfig , workspace : string ) : BuildPlan {
54126 const commands : CommandSpec [ ] = [ ] ;
@@ -114,10 +186,7 @@ export async function prepareMaven(config: RunConfig, workspace: string): Promis
114186 await validateRoot ( config ) ;
115187 const plan = planMaven ( config , workspace ) ;
116188 for ( const spec of plan . commands ) {
117- console . error ( `java-run:${ spec . stage } ` ) ;
118- await assertJavaArguments ( buildJava ( ) , spec . args , config . cwd ) ;
119- const result = await runCommand ( spec , { capture : true } ) ;
120- if ( result . exitCode !== 0 ) throw new CommandError ( spec , result ) ;
189+ await executeMaven ( spec ) ;
121190 }
122191 const pomFile = readFileSync ( join ( workspace , 'project-file.txt' ) , 'utf8' ) . trim ( ) ;
123192 if ( ! isAbsolute ( pomFile ) || ! existsSync ( pomFile ) ) throw new Error ( 'Maven 未返回有效的目标 POM 路径' ) ;
@@ -144,10 +213,7 @@ export async function needsMavenModule(config: RunConfig): Promise<boolean> {
144213export async function discoverMavenProjects ( config : RunConfig , workspace : string ) : Promise < { value : string ; label : string } [ ] > {
145214 const output = join ( workspace , 'module-list.xml' ) ;
146215 const spec = command ( { ...config , module : undefined } , [ `${ HELP_PLUGIN } :effective-pom` , `-Doutput=${ output } ` , '-q' ] , '读取 Maven 模块候选' ) ;
147- console . error ( `java-run:${ spec . stage } ` ) ;
148- await assertJavaArguments ( buildJava ( ) , spec . args , config . cwd ) ;
149- const result = await runCommand ( spec , { capture : true } ) ;
150- if ( result . exitCode !== 0 ) throw new CommandError ( spec , result ) ;
216+ await executeMaven ( spec ) ;
151217 const document = await parseStringPromise ( readFileSync ( output , 'utf8' ) , { explicitArray : false } ) ;
152218 const value = document . projects ?. project ?? document . project ;
153219 const projects = Array . isArray ( value ) ? value : value ? [ value ] : [ ] ;
0 commit comments