Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 89 additions & 4 deletions plugins/amazonq/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,17 +1,24 @@
// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

import org.jetbrains.intellij.platform.gradle.IntelliJPlatformType
import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import de.undercouch.gradle.tasks.download.Download
import org.jetbrains.intellij.platform.gradle.tasks.PrepareSandboxTask
import software.aws.toolkits.gradle.changelog.tasks.GeneratePluginChangeLog
import software.aws.toolkits.gradle.intellij.IdeFlavor
import software.aws.toolkits.gradle.intellij.IdeVersions
import software.aws.toolkits.gradle.intellij.toolkitIntelliJ

plugins {
id("toolkit-publishing-conventions")
id("toolkit-publish-root-conventions")
id("toolkit-jvm-conventions")
id("toolkit-testing")
id("de.undercouch.download")
}

buildscript {
dependencies {
classpath(libs.bundles.jackson)
}
}

val changelog = tasks.register<GeneratePluginChangeLog>("pluginChangeLog") {
Expand Down Expand Up @@ -51,3 +58,81 @@ tasks.check {
}
}
}

val downloadFlareManifest by tasks.registering(Download::class) {
src("https://aws-toolkit-language-servers.amazonaws.com/qAgenticChatServer/0/manifest.json")
dest(layout.buildDirectory.file("flare/manifest.json"))
onlyIfModified(true)
useETag(true)
}

data class FlareManifest(
val versions: List<FlareVersion>,
)

data class FlareVersion(
val serverVersion: String,
val thirdPartyLicenses: String,
val targets: List<FlareTarget>,
)

data class FlareTarget(
val platform: String,
val arch: String,
val contents: List<FlareContent>
)

data class FlareContent(
val url: String,
)

val downloadFlareArtifacts by tasks.registering(Download::class) {
dependsOn(downloadFlareManifest)
inputs.files(downloadFlareManifest)

val manifestFile = downloadFlareManifest.map { it.outputFiles.first() }
val manifest = manifestFile.map { jacksonObjectMapper().disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES).readValue(it.readText(), FlareManifest::class.java) }

// use darwin-aarch64 because its the smallest and we're going to throw away everything platform specific
val latest = manifest.map { it.versions.first() }
val latestVersion = latest.map { it.serverVersion }
val licensesUrl = latest.map { it.thirdPartyLicenses }
val darwin = latest.map { it.targets.first { it.platform == "darwin" && it.arch == "arm64" } }
val contentUrls = darwin.map { it.contents.map { content -> content.url } }

val destination = layout.buildDirectory.dir(latestVersion.map { "flare/$it" })
outputs.dir(destination)

src(contentUrls.zip(licensesUrl) { left, right -> left + right})
dest(destination)
onlyIfModified(true)
useETag(true)
}

val prepareBundledFlare by tasks.registering(Copy::class) {
dependsOn(downloadFlareArtifacts)
inputs.files(downloadFlareArtifacts)

val dest = layout.buildDirectory.dir("tmp/extractFlare")
into(dest)

from(downloadFlareArtifacts.map { it.outputFiles.filterNot { it.name.endsWith(".zip") } })
doLast {
copy {
into(dest)
includeEmptyDirs = false
downloadFlareArtifacts.get().outputFiles.filter { it.name.endsWith(".zip") }.forEach {
dest.get().file(it.parentFile.name).asFile.createNewFile()
from(zipTree(it)) {
include("*.js")
include("*.txt")
}
}
}
}
}

tasks.withType<PrepareSandboxTask>().configureEach {
intoChild(intellijPlatform.projectName.map { "$it/flare" })
.from(prepareBundledFlare)
}
9 changes: 9 additions & 0 deletions plugins/amazonq/shared/jetbrains-community/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,12 @@ dependencies {

testFixturesApi(testFixtures(project(":plugin-core:jetbrains-community")))
}

// hack because our test structure currently doesn't make complete sense
tasks.prepareTestSandbox {
val pluginXmlJar = project(":plugin-amazonq").tasks.jar

dependsOn(pluginXmlJar)
intoChild(intellijPlatform.projectName.map { "$it/lib" })
.from(pluginXmlJar)
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ import org.jetbrains.annotations.VisibleForTesting
import software.aws.toolkits.core.utils.error
import software.aws.toolkits.core.utils.getLogger
import software.aws.toolkits.core.utils.info
import software.aws.toolkits.core.utils.warn
import software.aws.toolkits.jetbrains.AwsPlugin
import software.aws.toolkits.jetbrains.AwsToolkit
import java.nio.file.Path

@Service
Expand Down Expand Up @@ -54,36 +57,43 @@ class ArtifactManager @NonInjectable internal constructor(private val manifestFe
return mutex.withLock {
coroutineScope {
async {
val manifest = manifestFetcher.fetch() ?: throw LspException(
"Language Support is not available, as manifest is missing.",
LspException.ErrorCode.MANIFEST_FETCH_FAILED
)
val lspVersions = getLSPVersionsFromManifestWithSpecifiedRange(manifest)

artifactHelper.removeDelistedVersions(lspVersions.deListedVersions)

if (lspVersions.inRangeVersions.isEmpty()) {
// No versions are found which are in the given range. Fallback to local lsp artifacts.
val localLspArtifacts = artifactHelper.getAllLocalLspArtifactsWithinManifestRange(DEFAULT_VERSION_RANGE)
if (localLspArtifacts.isNotEmpty()) {
return@async localLspArtifacts.first().first
try {
val manifest = manifestFetcher.fetch() ?: throw LspException(
"Language Support is not available, as manifest is missing.",
LspException.ErrorCode.MANIFEST_FETCH_FAILED
)
val lspVersions = getLSPVersionsFromManifestWithSpecifiedRange(manifest)

artifactHelper.removeDelistedVersions(lspVersions.deListedVersions)

if (lspVersions.inRangeVersions.isEmpty()) {
// No versions are found which are in the given range. Fallback to local lsp artifacts.
val localLspArtifacts = artifactHelper.getAllLocalLspArtifactsWithinManifestRange(DEFAULT_VERSION_RANGE)
if (localLspArtifacts.isNotEmpty()) {
return@async localLspArtifacts.first().first
}
throw LspException("Language server versions not found in manifest.", LspException.ErrorCode.NO_COMPATIBLE_LSP_VERSION)
}
throw LspException("Language server versions not found in manifest.", LspException.ErrorCode.NO_COMPATIBLE_LSP_VERSION)
}

val targetVersion = lspVersions.inRangeVersions.first()
val targetVersion = lspVersions.inRangeVersions.first()

// If there is an LSP Manifest with the same version
val target = getTargetFromLspManifest(targetVersion)
// Get Local LSP files and check if we can re-use existing LSP Artifacts
val artifactPath: Path = if (artifactHelper.getExistingLspArtifacts(targetVersion, target)) {
artifactHelper.getAllLocalLspArtifactsWithinManifestRange(DEFAULT_VERSION_RANGE).first().first
} else {
artifactHelper.tryDownloadLspArtifacts(project, targetVersion, target)
?: throw LspException("Failed to download LSP artifacts", LspException.ErrorCode.DOWNLOAD_FAILED)
// If there is an LSP Manifest with the same version
val target = getTargetFromLspManifest(targetVersion)
// Get Local LSP files and check if we can re-use existing LSP Artifacts
val artifactPath: Path = if (artifactHelper.getExistingLspArtifacts(targetVersion, target)) {
artifactHelper.getAllLocalLspArtifactsWithinManifestRange(DEFAULT_VERSION_RANGE).first().first
} else {
artifactHelper.tryDownloadLspArtifacts(project, targetVersion, target)
?: throw LspException("Failed to download LSP artifacts", LspException.ErrorCode.DOWNLOAD_FAILED)
}
artifactHelper.deleteOlderLspArtifacts(DEFAULT_VERSION_RANGE)
return@async artifactPath
} catch(e: Exception) {
logger.warn(e) { "Failed to resolve assets from Flare CDN" }
val path = AwsToolkit.PLUGINS_INFO[AwsPlugin.Q]?.path?.resolve("flare") ?: error("not even bundled")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want to notify here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

user should not care

logger.info { "Falling back to bundled assets at $path" }
return@async path
}
artifactHelper.deleteOlderLspArtifacts(DEFAULT_VERSION_RANGE)
return@async artifactPath
}
}.also {
artifactDeferred = it
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@

package software.aws.toolkits.jetbrains.services.amazonq.lsp.artifacts

import com.intellij.ide.plugins.PluginManagerCore
import com.intellij.openapi.extensions.PluginId
import com.intellij.testFramework.ProjectExtension
import com.intellij.util.text.SemVer
import io.mockk.Runs
Expand Down Expand Up @@ -51,29 +53,31 @@ class ArtifactManagerTest {
}

@Test
fun `fetch artifact fetcher throws exception if manifest is null`() = runTest {
fun `fetch artifact fetcher returns bundled if manifest is null`() = runTest {
every { manifestFetcher.fetch() }.returns(null)

val exception = assertThrows<LspException> {
artifactManager.fetchArtifact(projectExtension.project)
}
assertThat(exception)
.hasFieldOrPropertyWithValue("errorCode", LspException.ErrorCode.MANIFEST_FETCH_FAILED)

assertThat(artifactManager.fetchArtifact(projectExtension.project))
.isEqualTo(
PluginManagerCore.getPlugin(PluginId.getId("amazon.q"))?.pluginPath?.resolve("flare")
)
}

@Test
fun `fetch artifact does not have any valid lsp versions`() = runTest {
fun `fetch artifact does not have any valid lsp versions returns bundled`() = runTest {
every { manifestFetcher.fetch() }.returns(Manifest())

every { artifactManager.getLSPVersionsFromManifestWithSpecifiedRange(any()) }.returns(
ArtifactManager.LSPVersions(deListedVersions = emptyList(), inRangeVersions = emptyList())
)

val exception = assertThrows<LspException> {
artifactManager.fetchArtifact(projectExtension.project)
}
assertThat(exception)
.hasFieldOrPropertyWithValue("errorCode", LspException.ErrorCode.NO_COMPATIBLE_LSP_VERSION)
assertThat(artifactManager.fetchArtifact(projectExtension.project))
.isEqualTo(
PluginManagerCore.getPlugin(PluginId.getId("amazon.q"))?.pluginPath?.resolve("flare")
)
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@
package software.aws.toolkits.jetbrains

import com.intellij.ide.plugins.PluginManagerCore
import com.intellij.openapi.application.ApplicationManager

Check warning on line 7 in plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/AwsToolkit.kt

View workflow job for this annotation

GitHub Actions / Qodana Community for JVM

Unused import directive

Unused import directive
import com.intellij.openapi.extensions.PluginDescriptor
import com.intellij.openapi.extensions.PluginId
import java.nio.file.Path
import java.nio.file.Paths

Check warning on line 11 in plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/AwsToolkit.kt

View workflow job for this annotation

GitHub Actions / Qodana Community for JVM

Unused import directive

Unused import directive
import java.util.EnumMap

object AwsToolkit {
Expand Down Expand Up @@ -37,12 +37,7 @@
val version: String?
get() = descriptor?.version
val path: Path?
get() =
if (ApplicationManager.getApplication().isUnitTestMode) {
Paths.get(System.getProperty("plugin.path"))
} else {
descriptor?.pluginPath
}
get() = descriptor?.pluginPath
}

enum class AwsPlugin {
Expand Down
Loading