Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Prototype use of ide-starter with gradle-profiler #548

Draft
wants to merge 3 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
19 changes: 19 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ description = "A tool to profile and benchmark Gradle builds"

val gradleRuntime by configurations.creating
val profilerPlugins by configurations.creating
val ideImplementation by configurations.creating

dependencies {
implementation(libs.toolingApi)
Expand All @@ -42,6 +43,7 @@ dependencies {
because("To write JSON output")
}
implementation(project(":client-protocol"))
implementation(project(":ide-provisioning-api"))


gradleRuntime(gradleApi())
Expand All @@ -52,6 +54,7 @@ dependencies {
profilerPlugins(project(":studio-agent"))
profilerPlugins(project(":heap-dump"))
profilerPlugins(project(":studio-plugin"))
ideImplementation(project(":ide-provisioning"))

runtimeOnly("org.slf4j:slf4j-simple:1.7.10")
testImplementation(libs.bundles.testDependencies)
Expand Down Expand Up @@ -87,13 +90,29 @@ val generateHtmlReportJavaScript = tasks.register<NpxTask>("generateHtmlReportJa
args.addAll(source.absolutePath, "--outfile", output.get().asFile.absolutePath)
}

val listIdeProvisioningDependencies = tasks.register("listIdeProvisioningDependencies") {
val input = ideImplementation.minus(gradleRuntime)
val output = project.layout.buildDirectory.file("ide-provisioning/ide-provisioning.txt")
inputs.files(input)
outputs.file(output)
doLast {
output.get().asFile.writeText(input.joinToString("\n") { it.name })
}
}

tasks.processResources {
into("META-INF/jars") {
from(profilerPlugins.minus(gradleRuntime)) {
// Removing the version from the JARs here, since they are referenced by name in production code.
rename("""(.*)-\d\.\d.*\.jar""", "${'$'}1.jar")
}
}
into("META-INF/jars") {
from(ideImplementation.minus(gradleRuntime))
}
into("META-INF/classpath") {
from(listIdeProvisioningDependencies)
}
from(generateHtmlReportJavaScript)
}

Expand Down
7 changes: 7 additions & 0 deletions buildSrc/src/main/kotlin/profiler.java-library.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,13 @@ repositories {
}
}
mavenCentral()
maven {
url = uri("https://www.jetbrains.com/intellij-repository/releases")
}
maven {
url = uri("https://cache-redirector.jetbrains.com/intellij-dependencies")
}

}

java {
Expand Down
2 changes: 2 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ testAndroidStudioVersion = "2023.2.1.7"
testAndroidSdkVersion = "7.3.0"

[libraries]
commonIo = "commons-io:commons-io:2.11.0"
ideStarter = "com.jetbrains.intellij.tools:ide-starter-squashed:233.15026.9"
toolingApi = "org.gradle:gradle-tooling-api:7.2"

spock-core = { module = "org.spockframework:spock-core", version.ref = "spock" }
Expand Down
2 changes: 2 additions & 0 deletions settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ include("client-protocol")
include("instrumentation-support")
include("studio-agent")
include("studio-plugin")
include("ide-provisioning-api")
include("ide-provisioning")

rootProject.children.forEach {
it.projectDir = rootDir.resolve( "subprojects/${it.name}")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,18 @@
import org.gradle.profiler.GeneratedInitScript;
import org.gradle.profiler.GradleArgsCalculator;

import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.io.UncheckedIOException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.StandardCopyOption;
import java.util.List;
import java.util.stream.Collectors;

/**
* Represents some instrumentation that uses Gradle APIs and that is injected by gradle-profiler.
Expand Down Expand Up @@ -56,4 +61,20 @@ public static File unpackPlugin(String jarName) {
throw UncheckedException.throwAsUncheckedException(e);
}
}

public static URL[] getClasspath(String classPathName) {
List<String> classpathJars = readLines("/META-INF/classpath/" + classPathName + ".txt");
return classpathJars.stream()
.map(jar -> GradleInstrumentation.class.getResource("/META-INF/jars/" + jar))
.toArray(URL[]::new);
}

private static List<String> readLines(String resourcePath) {
try (BufferedReader reader = new BufferedReader(new InputStreamReader(GradleInstrumentation.class.getResourceAsStream(resourcePath)))) {
return reader.lines()
.collect(Collectors.toList());
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import org.gradle.profiler.BuildAction;
import org.gradle.profiler.GradleClient;
import org.gradle.profiler.result.BuildActionResult;
import org.gradle.profiler.studio.StudioGradleClient;

import java.util.List;

Expand Down
24 changes: 24 additions & 0 deletions src/main/java/org/gradle/profiler/studio/StudioGradleClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
import org.gradle.profiler.InvocationSettings;
import org.gradle.profiler.client.protocol.ServerConnection;
import org.gradle.profiler.client.protocol.messages.*;
import org.gradle.profiler.ide.RunIdeContext;
import org.gradle.profiler.ide.RunIdeStarter;
import org.gradle.profiler.instrument.GradleInstrumentation;
import org.gradle.profiler.result.BuildActionResult;
import org.gradle.profiler.studio.invoker.StudioBuildActionResult;
Expand All @@ -16,12 +18,15 @@
import org.gradle.profiler.studio.tools.StudioSandboxCreator.StudioSandbox;

import java.io.File;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.Path;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.ServiceLoader;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
Expand All @@ -32,6 +37,8 @@

public class StudioGradleClient implements GradleClient {



public enum CleanCacheMode {
BEFORE_SCENARIO,
BEFORE_BUILD,
Expand All @@ -48,11 +55,13 @@ public enum CleanCacheMode {
private final CleanCacheMode cleanCacheMode;
private final ExecutorService executor;
private final StudioSandbox sandbox;
private final InvocationSettings invocationSettings;
private boolean isFirstRun;

public StudioGradleClient(StudioGradleBuildConfiguration buildConfiguration, InvocationSettings invocationSettings, CleanCacheMode cleanCacheMode) {
this.isFirstRun = true;
this.cleanCacheMode = cleanCacheMode;
this.invocationSettings = invocationSettings;
Path studioInstallDir = invocationSettings.getStudioInstallDir().toPath();
Optional<File> studioSandboxDir = invocationSettings.getStudioSandboxDir();
this.sandbox = StudioSandboxCreator.createSandbox(studioSandboxDir.map(File::toPath).orElse(null));
Expand All @@ -64,7 +73,22 @@ public StudioGradleClient(StudioGradleBuildConfiguration buildConfiguration, Inv
this.executor = Executors.newSingleThreadExecutor();
}

private RunIdeContext newIdeContext() {
URL[] classpath = GradleInstrumentation.getClasspath("ide-provisioning");
RunIdeStarter ideStarter = ServiceLoader.load(RunIdeStarter.class, new URLClassLoader(classpath, RunIdeStarter.class.getClassLoader())).iterator().next();
return ideStarter.newContext(
invocationSettings.getProjectDir().getAbsolutePath(),
invocationSettings.getStudioInstallDir().getAbsolutePath()
);
}

public BuildActionResult sync(List<String> gradleArgs, List<String> jvmArgs) {
newIdeContext()
.withCommands()
.waitForSmartMode()
.importGradleProject()
.exitApp();

if (shouldCleanCache()) {
processController.runAndWaitToStop((connections) -> {
System.out.println("* Cleaning Android Studio cache, this will require a restart...");
Expand Down
12 changes: 12 additions & 0 deletions subprojects/ide-provisioning-api/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
plugins {
id("profiler.embedded-library")
}

description = "Api for IDE provisioning capabilities for Gradle profiler"


java {
toolchain {
languageVersion.set(JavaLanguageVersion.of(8))
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package org.gradle.profiler.ide;

public interface CommandChain {
Copy link
Member

Choose a reason for hiding this comment

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

🤔 This is looks like leaked to API implementation details. Not sure that it's reasonable for API client to manipulate Command terms for running an IDE. Client wants just to invoke fun sync(project, ide), probably add some VM arguments, but that's it.

WDYT?

CommandChain importGradleProject();
CommandChain waitForSmartMode();
CommandChain exitApp();
void run();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package org.gradle.profiler.ide;

public interface RunIdeContext {
RunIdeContext withSystemProperty(String key, String value);
CommandChain withCommands();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package org.gradle.profiler.ide;

public interface RunIdeStarter {
RunIdeContext newContext(String projectLocation, String ideLocation);
}
22 changes: 22 additions & 0 deletions subprojects/ide-provisioning/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
plugins {
id("profiler.embedded-library")
kotlin("jvm") version "1.9.22"
}

description = "IDE provisioning capabilities for Gradle profiler"

java {
toolchain {
languageVersion.set(JavaLanguageVersion.of(17))
}
}

dependencies {
implementation(project(":ide-provisioning-api"))
implementation(libs.ideStarter) {
exclude(group = "io.ktor")
}
implementation("org.kodein.di:kodein-di-jvm:7.20.2")
testImplementation(libs.bundles.testDependencies)
testImplementation(libs.commonIo)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package org.gradle.profiler.ide

import com.intellij.ide.starter.ide.IDETestContext
import com.intellij.tools.ide.performanceTesting.commands.exitApp
import com.intellij.tools.ide.performanceTesting.commands.importGradleProject
import com.intellij.tools.ide.performanceTesting.commands.waitForSmartMode

class CommandChainImpl(private val context: IDETestContext) : CommandChain {

private var commandChain = com.intellij.tools.ide.performanceTesting.commands.CommandChain()

override fun importGradleProject(): CommandChain {
commandChain = commandChain.importGradleProject()
return this
}

override fun waitForSmartMode(): CommandChain {
commandChain = commandChain.waitForSmartMode()
return this
}

override fun exitApp(): CommandChain {
commandChain = commandChain.exitApp()
return this
}

override fun run() {
context.runIDE(commands = commandChain)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package org.gradle.profiler.ide

import com.intellij.ide.starter.ide.IDETestContext

class RunIdeContextImpl(private val context: IDETestContext) : RunIdeContext {
override fun withSystemProperty(key: String, value: String): RunIdeContext {
context.applyVMOptionsPatch {
addSystemProperty(key, value)
}
return this
}

override fun withCommands(): CommandChain {
return CommandChainImpl(context)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package org.gradle.profiler.ide

import com.intellij.ide.starter.ide.IDETestContext
import com.intellij.ide.starter.ide.IdeDistributionFactory
import com.intellij.ide.starter.ide.IdeInstaller
import com.intellij.ide.starter.ide.InstalledIde
import com.intellij.ide.starter.ide.installer.ExistingIdeInstaller
import com.intellij.ide.starter.ide.installer.IdeInstallerFile
import com.intellij.ide.starter.models.IdeInfo
import com.intellij.ide.starter.models.TestCase
import com.intellij.ide.starter.path.GlobalPaths
import com.intellij.ide.starter.process.exec.ProcessExecutor
import com.intellij.ide.starter.project.LocalProjectInfo
import com.intellij.ide.starter.runner.TestContainer
import com.intellij.ide.starter.runner.TestContainerImpl
import com.intellij.openapi.util.SystemInfo
import com.intellij.openapi.util.io.FileUtil
import java.nio.file.Path
import java.nio.file.Paths
import kotlin.io.path.div
import kotlin.io.path.name
import kotlin.time.Duration.Companion.minutes

class RunIdeStarterImpl : RunIdeStarter {
override fun newContext(projectLocation: String, ideLocation: String): RunIdeContext {
val testVersion = "2023.2.3"
val ideInfo = IdeInfo(
productCode = "IC",
version = "2023.2",
// buildNumber = testVersion,
executableFileName = "idea",
fullName = "IntelliJ IDEA Community",
platformPrefix = "idea",
// getInstaller = { _ -> ExistingIdeInstaller(Paths.get(ideLocation)) }
)
val testCase = TestCase(
ideInfo,
LocalProjectInfo(Paths.get(projectLocation)),
)
val context = Starter.newContext("test", testCase)
return RunIdeContextImpl(context)
}

class ExistingIdeInstaller(private val installedIdePath: Path) : IdeInstaller {
override fun install(ideInfo: IdeInfo, includeRuntimeModuleRepository: Boolean): Pair<String, InstalledIde> {
val ideInstaller = IdeInstallerFile(installedIdePath, "locally-installed-ide")
val installDir = GlobalPaths.instance
.getCacheDirectoryFor("builds") / "${ideInfo.productCode}-${ideInstaller.buildNumber}"
installDir.toFile().deleteRecursively()
val installedIde = installedIdePath.toFile()
val destDir = installDir.resolve(installedIdePath.name).toFile()
if (SystemInfo.isMac) {
ProcessExecutor("copy app", null, 5.minutes, emptyMap(), listOf("ditto", installedIde.absolutePath, destDir.absolutePath)).start()
}
else {
FileUtil.copyDir(installedIde, destDir)
}
return Pair(
ideInstaller.buildNumber,
IdeDistributionFactory.installIDE(installDir.toFile(), ideInfo.executableFileName)
)
}
}

private object Starter {
private fun newTestContainer(): TestContainer<*> {
return TestContainerImpl()
}

fun newContext(testName: String, testCase: TestCase<*>, preserveSystemDir: Boolean = false): IDETestContext =
newTestContainer().initializeTestContext(
testName = testName,
testCase = testCase,
preserveSystemDir = preserveSystemDir
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
org.gradle.profiler.ide.RunIdeStarterImpl