Skip to content

Commit 7b5b557

Browse files
authored
feat(gumby): run mvnw if mvn fails #4290
Problem: When `mvn` fails, we want to run `mvnw` because that may succeed, since the user could have the Maven wrapper in their workspace despite not having Maven installed. Solution: Run `mvnw` when `mvn` fails. Also: 1. slight renaming 2. collect input in a simpler way (no need to use `MultiStepInputFlowController` since we only collect 1 piece of input).
1 parent 042c00e commit 7b5b557

File tree

4 files changed

+78
-59
lines changed

4 files changed

+78
-59
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"type": "Feature",
3+
"description": "Amazon Q CodeTransform: use Maven wrapper (if present) to copy dependencies if Maven not installed"
4+
}

src/codewhisperer/commands/startTransformByQ.ts

Lines changed: 31 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import * as fs from 'fs'
99
import * as os from 'os'
1010
import { getLogger } from '../../shared/logger'
1111
import * as CodeWhispererConstants from '../models/constants'
12-
import { transformByQState, StepProgress, DropdownStep, TransformByQReviewStatus } from '../models/model'
12+
import { transformByQState, StepProgress, TransformByQReviewStatus } from '../models/model'
1313
import {
1414
throwIfCancelled,
1515
startJob,
@@ -24,7 +24,6 @@ import {
2424
validateProjectSelection,
2525
} from '../service/transformByQHandler'
2626
import { QuickPickItem } from 'vscode'
27-
import { MultiStepInputFlowController } from '../../shared//multiStepInputFlowController'
2827
import path from 'path'
2928
import { sleep } from '../../shared/utilities/timeoutUtils'
3029
import { encodeHTML } from '../../shared/utilities/textUtilities'
@@ -73,38 +72,25 @@ interface UserInputState {
7372
project: QuickPickItem
7473
}
7574

76-
async function collectInputs(validProjects: vscode.QuickPickItem[] | undefined) {
77-
// const targetLanguages: QuickPickItem[] = CodeWhispererConstants.targetLanguages.map(label => ({ label }))
75+
async function collectInput(validProjects: vscode.QuickPickItem[]) {
7876
const state = {} as Partial<UserInputState>
79-
// only supporting target language of Java and target version of JDK17 for now, so skip to pickModule prompt
8077
transformByQState.setTargetJDKVersionToJDK17()
81-
await MultiStepInputFlowController.run(input => pickProject(input, state, validProjects))
82-
// await MultiStepInputFlowController.run(input => pickTargetLanguage(input, state, targetLanguages, validModules))
83-
return state as UserInputState
84-
}
85-
86-
async function pickProject(
87-
input: MultiStepInputFlowController,
88-
state: Partial<UserInputState>,
89-
validProjects: vscode.QuickPickItem[] | undefined
90-
) {
91-
const pick = await input.showQuickPick({
78+
const pick = await vscode.window.showQuickPick(validProjects, {
9279
title: CodeWhispererConstants.transformByQWindowTitle,
93-
step: DropdownStep.STEP_1,
94-
totalSteps: DropdownStep.STEP_1,
95-
placeholder: CodeWhispererConstants.selectModulePrompt,
96-
items: validProjects!,
97-
shouldResume: () => Promise.resolve(false),
80+
placeHolder: CodeWhispererConstants.selectModulePrompt,
9881
})
99-
state.project = pick
100-
transformByQState.setProjectName(encodeHTML(state.project.label)) // encode to avoid HTML injection risk
82+
if (pick) {
83+
state.project = pick
84+
transformByQState.setProjectName(encodeHTML(state.project.label)) // encode to avoid HTML injection risk
85+
}
86+
return state as UserInputState
10187
}
10288

10389
export async function startTransformByQ() {
10490
let intervalId = undefined
10591

106-
// 1: Validate inputs. If failed, Error will be thrown and execution stops
107-
const userInputState = await validateTransformJob()
92+
// Validate inputs. If failed, Error will be thrown and execution stops
93+
const userInputState = await validateTransformationJob()
10894

10995
// Set the default state variables for our store and the UI
11096
await setTransformationToRunningState(userInputState)
@@ -119,29 +105,28 @@ export async function startTransformByQ() {
119105
}, CodeWhispererConstants.progressIntervalMs)
120106

121107
// step 1: CreateCodeUploadUrl and upload code
122-
const uploadId = await preTransformUploadCode(userInputState)
108+
const uploadId = await preTransformationUploadCode(userInputState)
123109

124110
// step 2: StartJob and store the returned jobId in TransformByQState
125-
const jobId = await transformStartJob(uploadId)
111+
const jobId = await startTransformationJob(uploadId)
126112

127113
// step 3 (intermediate step): show transformation-plan.md file
128-
// TO-DO: on IDE restart, resume here if a job was ongoing
129-
await pollTransformationStatusTillPlanReady(jobId)
114+
await pollTransformationStatusUntilPlanReady(jobId)
130115

131116
// step 4: poll until artifacts are ready to download
132-
const status = await pollTransformationTillComplete(jobId)
117+
const status = await pollTransformationStatusUntilComplete(jobId)
133118

134119
// Set the result state variables for our store and the UI
135120
await finalizeTransformationJob(status)
136121
} catch (error: any) {
137-
await modernizationJobErrorHandler(error)
122+
await transformationJobErrorHandler(error)
138123
} finally {
139-
await postTransformJob(userInputState)
140-
await cleanupTransformJob(intervalId)
124+
await postTransformationJob(userInputState)
125+
await cleanupTransformationJob(intervalId)
141126
}
142127
}
143128

144-
export async function preTransformUploadCode(userInputState: UserInputState) {
129+
export async function preTransformationUploadCode(userInputState: UserInputState) {
145130
await vscode.commands.executeCommand('aws.amazonq.refresh')
146131
await vscode.commands.executeCommand('aws.amazonq.transformationHub.focus')
147132

@@ -173,7 +158,7 @@ export async function preTransformUploadCode(userInputState: UserInputState) {
173158
return uploadId
174159
}
175160

176-
export async function transformStartJob(uploadId: string) {
161+
export async function startTransformationJob(uploadId: string) {
177162
let jobId = ''
178163
try {
179164
jobId = await startJob(uploadId)
@@ -197,7 +182,7 @@ export async function transformStartJob(uploadId: string) {
197182
return jobId
198183
}
199184

200-
export async function pollTransformationStatusTillPlanReady(jobId: string) {
185+
export async function pollTransformationStatusUntilPlanReady(jobId: string) {
201186
try {
202187
await pollTransformationJob(jobId, CodeWhispererConstants.validStatesForGettingPlan)
203188
} catch (error) {
@@ -223,7 +208,7 @@ export async function pollTransformationStatusTillPlanReady(jobId: string) {
223208
throwIfCancelled()
224209
}
225210

226-
export async function pollTransformationTillComplete(jobId: string) {
211+
export async function pollTransformationStatusUntilComplete(jobId: string) {
227212
let status = ''
228213
try {
229214
status = await pollTransformationJob(jobId, CodeWhispererConstants.validStatesForCheckingDownloadUrl)
@@ -260,15 +245,19 @@ export async function finalizeTransformationJob(status: string) {
260245
sessionPlanProgress['returnCode'] = StepProgress.Succeeded
261246
}
262247

263-
export async function validateTransformJob() {
248+
export async function validateTransformationJob() {
264249
let openProjects: vscode.QuickPickItem[] = []
265250
try {
266251
openProjects = await getOpenProjects()
267252
} catch (err) {
268253
getLogger().error('Failed to get open projects: ', err)
269254
throw err
270255
}
271-
const userInputState = await collectInputs(openProjects)
256+
const userInputState = await collectInput(openProjects)
257+
258+
if (!userInputState.project) {
259+
throw new ToolkitError('No project selected', { code: 'NoProjectSelected' })
260+
}
272261

273262
try {
274263
await validateProjectSelection(userInputState.project)
@@ -333,7 +322,7 @@ export async function setTransformationToRunningState(userInputState: UserInputS
333322
await vscode.commands.executeCommand('aws.amazonq.refresh')
334323
}
335324

336-
export async function postTransformJob(userInputState: UserInputState) {
325+
export async function postTransformationJob(userInputState: UserInputState) {
337326
await vscode.commands.executeCommand('setContext', 'gumby.isTransformAvailable', true)
338327
const durationInMs = calculateTotalLatency(codeTransformTelemetryState.getStartTime())
339328
const resultStatusMessage = codeTransformTelemetryState.getResultStatus()
@@ -368,7 +357,7 @@ export async function postTransformJob(userInputState: UserInputState) {
368357
}
369358
}
370359

371-
export async function modernizationJobErrorHandler(error: any) {
360+
export async function transformationJobErrorHandler(error: any) {
372361
if (transformByQState.isCancelled()) {
373362
codeTransformTelemetryState.setResultStatus('JobCancelled')
374363
try {
@@ -403,7 +392,7 @@ export async function modernizationJobErrorHandler(error: any) {
403392
getLogger().error('Amazon Q Code Transform', error)
404393
}
405394

406-
export async function cleanupTransformJob(intervalId: NodeJS.Timeout | undefined) {
395+
export async function cleanupTransformationJob(intervalId: NodeJS.Timeout | undefined) {
407396
clearInterval(intervalId)
408397
transformByQState.setJobDefaults()
409398
await vscode.commands.executeCommand('setContext', 'gumby.isStopButtonAvailable', false)

src/codewhisperer/models/constants.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -289,7 +289,7 @@ export const selectTargetVersionPrompt = 'Select the target version'
289289

290290
export const selectModulePrompt = 'Select the module you want to transform'
291291

292-
export const transformByQWindowTitle = 'Transform'
292+
export const transformByQWindowTitle = 'Amazon Q CodeTransformation'
293293

294294
export const stopTransformByQMessage = 'Stop Transformation?'
295295

src/codewhisperer/service/transformByQHandler.ts

Lines changed: 42 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import { spawnSync } from 'child_process'
1818
import AdmZip from 'adm-zip'
1919
import fetch from '../../common/request'
2020
import globals from '../../shared/extensionGlobals'
21-
import { telemetry } from '../../shared/telemetry/telemetry'
21+
import { CodeTransformMavenBuildCommand, telemetry } from '../../shared/telemetry/telemetry'
2222
import { ToolkitError } from '../../shared/errors'
2323
import { codeTransformTelemetryState } from '../../amazonqGumby/telemetry/codeTransformTelemetryState'
2424
import { calculateTotalLatency, javapOutputToTelemetryValue } from '../../amazonqGumby/telemetry/codeTransformTelemetry'
@@ -310,12 +310,24 @@ function getFilesRecursively(dir: string, isDependenciesFolder: boolean): string
310310
return files
311311
}
312312

313-
function getProjectDependencies(modulePath: string): string[] {
313+
function getProjectDependencies(buildCommand: CodeTransformMavenBuildCommand, modulePath: string): string[] {
314314
// Make temp directory
315315
const folderName = `${CodeWhispererConstants.dependencyFolderName}${Date.now()}`
316316
const folderPath = path.join(os.tmpdir(), folderName)
317317

318-
const baseCommand = 'mvn'
318+
let baseCommand = buildCommand as string
319+
if (baseCommand === 'mvnw') {
320+
baseCommand = './mvnw'
321+
if (os.platform() === 'win32') {
322+
baseCommand = './mvnw.cmd'
323+
}
324+
const executableName = baseCommand.substring(2) // remove the './' part
325+
const executablePath = path.join(modulePath, executableName)
326+
if (!fs.existsSync(executablePath)) {
327+
throw new ToolkitError('Maven Wrapper not found', { code: 'MavenWrapperNotFound' })
328+
}
329+
}
330+
319331
const args = [
320332
'dependency:copy-dependencies',
321333
'-DoutputDirectory=' + folderPath,
@@ -326,8 +338,7 @@ function getProjectDependencies(modulePath: string): string[] {
326338
const spawnResult = spawnSync(baseCommand, args, { cwd: modulePath, shell: true, encoding: 'utf-8' })
327339

328340
if (spawnResult.error || spawnResult.status !== 0) {
329-
void vscode.window.showErrorMessage(CodeWhispererConstants.dependencyErrorMessage)
330-
getLogger().error('CodeTransform: Error in running Maven command = ')
341+
getLogger().error(`CodeTransform: Error in running Maven command ${baseCommand} = `)
331342
// Maven command can still go through and still return an error. Won't be caught in spawnResult.error in this case
332343
if (spawnResult.error) {
333344
getLogger().error(spawnResult.error)
@@ -336,7 +347,7 @@ function getProjectDependencies(modulePath: string): string[] {
336347
}
337348
telemetry.codeTransform_mvnBuildFailed.emit({
338349
codeTransformSessionId: codeTransformTelemetryState.getSessionId(),
339-
codeTransformMavenBuildCommand: baseCommand,
350+
codeTransformMavenBuildCommand: buildCommand,
340351
result: MetadataResult.Fail,
341352
reason: spawnResult.error ? spawnResult.error.message : spawnResult.stdout,
342353
})
@@ -355,15 +366,30 @@ export async function zipCode(modulePath: string) {
355366
throwIfCancelled()
356367

357368
let dependencyFolderInfo: string[] = []
358-
let mavenFailed = false
369+
let mavenWrapperFailed = false
359370
try {
360-
dependencyFolderInfo = getProjectDependencies(modulePath)
371+
dependencyFolderInfo = getProjectDependencies('mvnw', modulePath)
361372
} catch (err) {
362-
mavenFailed = true
373+
mavenWrapperFailed = true
374+
}
375+
376+
let mavenFailed = false
377+
if (mavenWrapperFailed) {
378+
try {
379+
dependencyFolderInfo = getProjectDependencies('mvn', modulePath)
380+
} catch (err) {
381+
mavenFailed = true
382+
}
383+
}
384+
385+
const copyDependenciesFailed = mavenFailed && mavenWrapperFailed
386+
387+
if (copyDependenciesFailed) {
388+
void vscode.window.showErrorMessage(CodeWhispererConstants.dependencyErrorMessage)
363389
}
364390

365-
const dependencyFolderPath = !mavenFailed ? dependencyFolderInfo[0] : ''
366-
const dependencyFolderName = !mavenFailed ? dependencyFolderInfo[1] : ''
391+
const dependencyFolderPath = !copyDependenciesFailed ? dependencyFolderInfo[0] : ''
392+
const dependencyFolderName = !copyDependenciesFailed ? dependencyFolderInfo[1] : ''
367393

368394
throwIfCancelled()
369395

@@ -379,11 +405,11 @@ export async function zipCode(modulePath: string) {
379405
throwIfCancelled()
380406

381407
let dependencyFiles: string[] = []
382-
if (!mavenFailed && fs.existsSync(dependencyFolderPath)) {
408+
if (!copyDependenciesFailed && fs.existsSync(dependencyFolderPath)) {
383409
dependencyFiles = getFilesRecursively(dependencyFolderPath, true)
384410
}
385411

386-
if (!mavenFailed && dependencyFiles.length > 0) {
412+
if (!copyDependenciesFailed && dependencyFiles.length > 0) {
387413
for (const file of dependencyFiles) {
388414
const relativePath = path.relative(dependencyFolderPath, file)
389415
const paddedPath = path.join(`dependencies/${dependencyFolderName}`, relativePath)
@@ -403,18 +429,18 @@ export async function zipCode(modulePath: string) {
403429

404430
const tempFilePath = path.join(os.tmpdir(), 'zipped-code.zip')
405431
fs.writeFileSync(tempFilePath, zip.toBuffer())
406-
if (!mavenFailed) {
432+
if (!copyDependenciesFailed) {
407433
fs.rmSync(dependencyFolderPath, { recursive: true, force: true })
408434
}
409435

410436
// for now, use the pass/fail status of the maven command to determine this metric status
411-
const mavenStatus = mavenFailed ? MetadataResult.Fail : MetadataResult.Pass
437+
const mavenStatus = copyDependenciesFailed ? MetadataResult.Fail : MetadataResult.Pass
412438
telemetry.codeTransform_jobCreateZipEndTime.emit({
413439
codeTransformSessionId: codeTransformTelemetryState.getSessionId(),
414440
codeTransformTotalByteSize: (await fs.promises.stat(tempFilePath)).size,
415441
codeTransformRunTimeLatency: calculateTotalLatency(zipStartTime),
416442
result: mavenStatus,
417-
reason: mavenFailed ? 'MavenCommandFailed' : undefined,
443+
reason: copyDependenciesFailed ? 'MavenCommandsFailed' : undefined,
418444
})
419445
return tempFilePath
420446
}

0 commit comments

Comments
 (0)