From e9cef1424c31a374aff99eb1e1ab7251f640b70c Mon Sep 17 00:00:00 2001 From: Sergey Opivalov Date: Mon, 15 Jan 2024 11:59:45 +0300 Subject: [PATCH 1/3] Add IDEA provisioning Signed-off-by: Sergey Opivalov --- settings.gradle.kts | 1 + subprojects/ide-provisioning/build.gradle.kts | 29 ++++++++ .../profiler/ide/DefaultIdeProvider.java | 34 +++++++++ .../java/org/gradle/profiler/ide/Ide.java | 9 +++ .../org/gradle/profiler/ide/IdeProvider.java | 9 +++ .../org/gradle/profiler/ide/idea/IDEA.java | 18 +++++ .../profiler/ide/idea/IDEAProvider.java | 74 +++++++++++++++++++ .../org/gradle/profiler/TeeOutputStream.java | 52 +++++++++++++ .../ide/AbstractIdeProvisioningTest.groovy | 28 +++++++ .../profiler/ide/IdeProviderTest.groovy | 33 +++++++++ 10 files changed, 287 insertions(+) create mode 100644 subprojects/ide-provisioning/build.gradle.kts create mode 100644 subprojects/ide-provisioning/src/main/java/org/gradle/profiler/ide/DefaultIdeProvider.java create mode 100644 subprojects/ide-provisioning/src/main/java/org/gradle/profiler/ide/Ide.java create mode 100644 subprojects/ide-provisioning/src/main/java/org/gradle/profiler/ide/IdeProvider.java create mode 100644 subprojects/ide-provisioning/src/main/java/org/gradle/profiler/ide/idea/IDEA.java create mode 100644 subprojects/ide-provisioning/src/main/java/org/gradle/profiler/ide/idea/IDEAProvider.java create mode 100644 subprojects/ide-provisioning/src/test/groovy/org/gradle/profiler/TeeOutputStream.java create mode 100644 subprojects/ide-provisioning/src/test/groovy/org/gradle/profiler/ide/AbstractIdeProvisioningTest.groovy create mode 100644 subprojects/ide-provisioning/src/test/groovy/org/gradle/profiler/ide/IdeProviderTest.groovy diff --git a/settings.gradle.kts b/settings.gradle.kts index 0549daeb..467ccbc8 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -14,6 +14,7 @@ include("client-protocol") include("instrumentation-support") include("studio-agent") include("studio-plugin") +include("ide-provisioning") rootProject.children.forEach { it.projectDir = rootDir.resolve( "subprojects/${it.name}") diff --git a/subprojects/ide-provisioning/build.gradle.kts b/subprojects/ide-provisioning/build.gradle.kts new file mode 100644 index 00000000..3c6b34c5 --- /dev/null +++ b/subprojects/ide-provisioning/build.gradle.kts @@ -0,0 +1,29 @@ +plugins { + id("profiler.embedded-library") +} + +java { + toolchain { + languageVersion.set(JavaLanguageVersion.of(17)) + } +} + +repositories { + maven { url = uri("https://www.jetbrains.com/intellij-repository/releases") } + maven { url = uri("https://cache-redirector.jetbrains.com/intellij-dependencies") } + + maven { url = uri("https://cache-redirector.jetbrains.com/maven-central") } + maven { url = uri("https://www.jetbrains.com/intellij-repository/snapshots") } + maven { + url = uri("https://cache-redirector.jetbrains.com/packages.jetbrains.team/maven/p/grazi/grazie-platform-public") + } +} + +dependencies { + implementation("com.jetbrains.intellij.tools:ide-starter-squashed:233.13135.103") { + exclude(group = "io.ktor") + exclude(group = "com.jetbrains.infra") + exclude(group = "com.jetbrains.intellij.remoteDev") + } + testImplementation(libs.bundles.testDependencies) +} diff --git a/subprojects/ide-provisioning/src/main/java/org/gradle/profiler/ide/DefaultIdeProvider.java b/subprojects/ide-provisioning/src/main/java/org/gradle/profiler/ide/DefaultIdeProvider.java new file mode 100644 index 00000000..a64d0335 --- /dev/null +++ b/subprojects/ide-provisioning/src/main/java/org/gradle/profiler/ide/DefaultIdeProvider.java @@ -0,0 +1,34 @@ +package org.gradle.profiler.ide; + +import kotlin.io.FilesKt; +import org.gradle.profiler.ide.idea.IDEA; +import org.gradle.profiler.ide.idea.IDEAProvider; + +import java.io.File; +import java.nio.file.Path; + +public class DefaultIdeProvider implements IdeProvider { + + private final IDEAProvider ideaProvider; + + public DefaultIdeProvider(IDEAProvider ideaProvider) { + this.ideaProvider = ideaProvider; + } + + @Override + public File provideIde(Ide ide, Path homeDir, Path downloadsDir) { + File result; + if (ide instanceof IDEA) { + result = ideaProvider.provideIde((IDEA) ide, homeDir, downloadsDir); + } else { + throw new IllegalArgumentException("Unknown IDE to provide"); + } + + cleanup(homeDir, downloadsDir); + return result; + } + + private void cleanup(Path homeDir, Path downloadsDir) { + FilesKt.deleteRecursively(downloadsDir.toFile()); + } +} diff --git a/subprojects/ide-provisioning/src/main/java/org/gradle/profiler/ide/Ide.java b/subprojects/ide-provisioning/src/main/java/org/gradle/profiler/ide/Ide.java new file mode 100644 index 00000000..e66d8ba2 --- /dev/null +++ b/subprojects/ide-provisioning/src/main/java/org/gradle/profiler/ide/Ide.java @@ -0,0 +1,9 @@ +package org.gradle.profiler.ide; + +import org.jetbrains.annotations.NotNull; + +public interface Ide { + @NotNull + String getVersion(); +} + diff --git a/subprojects/ide-provisioning/src/main/java/org/gradle/profiler/ide/IdeProvider.java b/subprojects/ide-provisioning/src/main/java/org/gradle/profiler/ide/IdeProvider.java new file mode 100644 index 00000000..ed7f90f4 --- /dev/null +++ b/subprojects/ide-provisioning/src/main/java/org/gradle/profiler/ide/IdeProvider.java @@ -0,0 +1,9 @@ +package org.gradle.profiler.ide; + +import java.io.File; +import java.nio.file.Path; + +public interface IdeProvider { + + File provideIde(T ide, Path homeDir, Path downloadsDir); +} diff --git a/subprojects/ide-provisioning/src/main/java/org/gradle/profiler/ide/idea/IDEA.java b/subprojects/ide-provisioning/src/main/java/org/gradle/profiler/ide/idea/IDEA.java new file mode 100644 index 00000000..61d1df81 --- /dev/null +++ b/subprojects/ide-provisioning/src/main/java/org/gradle/profiler/ide/idea/IDEA.java @@ -0,0 +1,18 @@ +package org.gradle.profiler.ide.idea; + +import org.gradle.profiler.ide.Ide; +import org.jetbrains.annotations.NotNull; + +public class IDEA implements Ide { + public static IDEA LATEST = new IDEA(""); + private final String version; + public IDEA(String version) { + this.version = version; + } + + @NotNull + @Override + public String getVersion() { + return version; + } +} diff --git a/subprojects/ide-provisioning/src/main/java/org/gradle/profiler/ide/idea/IDEAProvider.java b/subprojects/ide-provisioning/src/main/java/org/gradle/profiler/ide/idea/IDEAProvider.java new file mode 100644 index 00000000..b9fb8a2e --- /dev/null +++ b/subprojects/ide-provisioning/src/main/java/org/gradle/profiler/ide/idea/IDEAProvider.java @@ -0,0 +1,74 @@ +package org.gradle.profiler.ide.idea; + +import com.intellij.ide.starter.community.PublicIdeDownloader; +import com.intellij.ide.starter.community.model.BuildType; +import com.intellij.ide.starter.ide.IdeArchiveExtractor; +import com.intellij.ide.starter.ide.IdeDownloader; +import com.intellij.ide.starter.ide.installer.IdeInstallerFile; +import com.intellij.ide.starter.models.IdeInfo; +import org.gradle.profiler.ide.IdeProvider; +import org.jetbrains.annotations.Nullable; + +import java.io.File; +import java.nio.file.Path; +import java.util.Collections; + +public class IDEAProvider implements IdeProvider { + private final IdeDownloader ideDownloader; + private final IdeArchiveExtractor ideArchiveExtractor; + + public IDEAProvider() { + this.ideDownloader = PublicIdeDownloader.INSTANCE; + this.ideArchiveExtractor = IdeArchiveExtractor.INSTANCE; + } + + @Override + public File provideIde(IDEA ide, Path homeDir, Path downloadsDir) { + IdeInfo ideInfo = new IdeInfo( + "IC", + "Idea", + "idea", + BuildType.EAP.getType(), + Collections.emptyList(), + "", // latest + ide.getVersion(), + null, + null, + "IDEA Community", + info -> null + ); + + String version = ideInfo.getVersion().isEmpty() + ? "latest" + : ideInfo.getVersion(); + + File unpackDir = homeDir + .resolve(ideInfo.getInstallerFilePrefix()) + .resolve(version) + .toFile(); + + File unpackedIde = getUnpackedIde(unpackDir); + + if (unpackedIde != null) { + System.out.println("Downloading is skipped, get " + ideInfo.getFullName() + " from cache"); + return unpackedIde; + } + + IdeInstallerFile installerFile = ideDownloader.downloadIdeInstaller(ideInfo, downloadsDir); + ideArchiveExtractor.unpackIdeIfNeeded(installerFile.getInstallerFile().toFile(), unpackDir); + + return unpackDir.listFiles()[0]; + } + + @Nullable + private static File getUnpackedIde(File unpackDir) { + File[] unpackedFiles = unpackDir.listFiles(); + if (unpackedFiles == null || unpackedFiles.length == 0) { + return null; + } + if (unpackedFiles.length == 1) { + return unpackedFiles[0]; + } + throw new IllegalStateException("Unexpected content in " + unpackDir); + } +} diff --git a/subprojects/ide-provisioning/src/test/groovy/org/gradle/profiler/TeeOutputStream.java b/subprojects/ide-provisioning/src/test/groovy/org/gradle/profiler/TeeOutputStream.java new file mode 100644 index 00000000..11e5ebe4 --- /dev/null +++ b/subprojects/ide-provisioning/src/test/groovy/org/gradle/profiler/TeeOutputStream.java @@ -0,0 +1,52 @@ +package org.gradle.profiler; + +import java.io.IOException; +import java.io.OutputStream; +import java.util.Arrays; +import java.util.List; + +public class TeeOutputStream extends OutputStream { + private final List targets; + + public TeeOutputStream(OutputStream... targets) { + this.targets = Arrays.asList(targets); + } + + @Override + public void write(int b) throws IOException { + withAll(stream -> stream.write(b)); + } + + @Override + public void write(byte[] b, int off, int len) throws IOException { + withAll(stream -> stream.write(b, off, len)); + } + + @Override + public void flush() throws IOException { + withAll(stream -> stream.flush()); + } + + @Override + public void close() throws IOException { + withAll(stream -> stream.close()); + } + + private void withAll(IOAction action) throws IOException { + IOException failure = null; + for (OutputStream target : targets) { + try { + action.withStream(target); + } catch (IOException e) { + failure = e; + } + } + if (failure != null) { + throw failure; + } + } + + private interface IOAction { + void withStream(OutputStream stream) throws IOException; + } +} diff --git a/subprojects/ide-provisioning/src/test/groovy/org/gradle/profiler/ide/AbstractIdeProvisioningTest.groovy b/subprojects/ide-provisioning/src/test/groovy/org/gradle/profiler/ide/AbstractIdeProvisioningTest.groovy new file mode 100644 index 00000000..2f2b8a1f --- /dev/null +++ b/subprojects/ide-provisioning/src/test/groovy/org/gradle/profiler/ide/AbstractIdeProvisioningTest.groovy @@ -0,0 +1,28 @@ +package org.gradle.profiler.ide + +import org.gradle.profiler.TeeOutputStream +import org.junit.Rule +import org.junit.rules.TemporaryFolder +import spock.lang.Specification + +class AbstractIdeProvisioningTest extends Specification { + + private ByteArrayOutputStream outputBuffer + + @Rule + protected TemporaryFolder tmpDir = new TemporaryFolder(new File("build/tmp/")) + + def setup() { + outputBuffer = new ByteArrayOutputStream() + System.out = new PrintStream(new TeeOutputStream(System.out, outputBuffer)) + } + + protected void outputContains(String content) { + assert getOutput().contains(content.trim()) + } + + protected String getOutput() { + System.out.flush() + return new String(outputBuffer.toByteArray()) + } +} diff --git a/subprojects/ide-provisioning/src/test/groovy/org/gradle/profiler/ide/IdeProviderTest.groovy b/subprojects/ide-provisioning/src/test/groovy/org/gradle/profiler/ide/IdeProviderTest.groovy new file mode 100644 index 00000000..71549fe4 --- /dev/null +++ b/subprojects/ide-provisioning/src/test/groovy/org/gradle/profiler/ide/IdeProviderTest.groovy @@ -0,0 +1,33 @@ +package org.gradle.profiler.ide + + +import org.gradle.profiler.ide.idea.IDEA +import org.gradle.profiler.ide.idea.IDEAProvider + +class IdeProviderTest extends AbstractIdeProvisioningTest { + + def "can provide IDEA"() { + given: + def workDir = tmpDir.newFolder().toPath().toAbsolutePath() + def downloadsDir = workDir.resolve("downloads") + def ideHomeDir = workDir.resolve("ide") + def ideProvider = new DefaultIdeProvider(new IDEAProvider()) + + when: + def ide = ideProvider.provideIde(IDEA.LATEST, ideHomeDir, downloadsDir) + + then: + outputContains("Downloading https://") + assert ide.exists() + + when: + def ide2 = ideProvider.provideIde(IDEA.LATEST, ideHomeDir, downloadsDir) + + then: + outputContains("Downloading is skipped, get IDEA Community from cache") + assert ide == ide2 + + and: + assert !downloadsDir.toFile().exists() + } +} From cd27166e21252cb044b428818246afec54de2944 Mon Sep 17 00:00:00 2001 From: Sergey Opivalov Date: Mon, 15 Jan 2024 12:57:47 +0300 Subject: [PATCH 2/3] Remove explicit asserts in test Signed-off-by: Sergey Opivalov --- .../groovy/org/gradle/profiler/ide/IdeProviderTest.groovy | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/subprojects/ide-provisioning/src/test/groovy/org/gradle/profiler/ide/IdeProviderTest.groovy b/subprojects/ide-provisioning/src/test/groovy/org/gradle/profiler/ide/IdeProviderTest.groovy index 71549fe4..a0eebff8 100644 --- a/subprojects/ide-provisioning/src/test/groovy/org/gradle/profiler/ide/IdeProviderTest.groovy +++ b/subprojects/ide-provisioning/src/test/groovy/org/gradle/profiler/ide/IdeProviderTest.groovy @@ -18,16 +18,16 @@ class IdeProviderTest extends AbstractIdeProvisioningTest { then: outputContains("Downloading https://") - assert ide.exists() + ide.exists() when: def ide2 = ideProvider.provideIde(IDEA.LATEST, ideHomeDir, downloadsDir) then: outputContains("Downloading is skipped, get IDEA Community from cache") - assert ide == ide2 + ide == ide2 and: - assert !downloadsDir.toFile().exists() + !downloadsDir.toFile().exists() } } From cf92c324ed83a5a2656d0844a812ee15b1789ec2 Mon Sep 17 00:00:00 2001 From: Sergey Opivalov Date: Mon, 15 Jan 2024 14:49:49 +0300 Subject: [PATCH 3/3] Use TeeOutputStream from common-io Signed-off-by: Sergey Opivalov --- build.gradle.kts | 2 +- gradle/libs.versions.toml | 2 + .../java/org/gradle/profiler/CommandExec.java | 2 + .../java/org/gradle/profiler/Logging.java | 2 + .../org/gradle/profiler/TeeOutputStream.java | 52 ------------------- .../profiler/AbstractIntegrationTest.groovy | 1 + subprojects/ide-provisioning/build.gradle.kts | 9 +--- .../org/gradle/profiler/TeeOutputStream.java | 52 ------------------- .../ide/AbstractIdeProvisioningTest.groovy | 2 +- 9 files changed, 11 insertions(+), 113 deletions(-) delete mode 100644 src/main/java/org/gradle/profiler/TeeOutputStream.java delete mode 100644 subprojects/ide-provisioning/src/test/groovy/org/gradle/profiler/TeeOutputStream.java diff --git a/build.gradle.kts b/build.gradle.kts index 14cad7c1..ff4a2ccc 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -33,7 +33,7 @@ dependencies { implementation("com.github.javaparser:javaparser-core:3.18.0") implementation("net.sf.jopt-simple:jopt-simple:5.0.4") implementation("org.apache.ant:ant-compress:1.5") - implementation("commons-io:commons-io:2.6") + implementation(libs.commonIo) implementation("org.openjdk.jmc:flightrecorder:8.0.1") implementation("com.googlecode.plist:dd-plist:1.23") { because("To extract launch details from Android Studio installation") diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 38aa94b5..333ccb45 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -3,6 +3,8 @@ groovy = "3.0.15" spock = "2.1-groovy-3.0" [libraries] +commonIo = "commons-io:commons-io:2.11.0" +ideStarter = "com.jetbrains.intellij.tools:ide-starter-squashed:233.13135.103" toolingApi = "org.gradle:gradle-tooling-api:7.2" spock-core = { module = "org.spockframework:spock-core", version.ref = "spock" } diff --git a/src/main/java/org/gradle/profiler/CommandExec.java b/src/main/java/org/gradle/profiler/CommandExec.java index 49008bef..9a6a6690 100644 --- a/src/main/java/org/gradle/profiler/CommandExec.java +++ b/src/main/java/org/gradle/profiler/CommandExec.java @@ -1,5 +1,7 @@ package org.gradle.profiler; +import org.apache.commons.io.output.TeeOutputStream; + import javax.annotation.Nullable; import java.io.ByteArrayOutputStream; import java.io.File; diff --git a/src/main/java/org/gradle/profiler/Logging.java b/src/main/java/org/gradle/profiler/Logging.java index d076a128..66e28bf5 100644 --- a/src/main/java/org/gradle/profiler/Logging.java +++ b/src/main/java/org/gradle/profiler/Logging.java @@ -1,5 +1,7 @@ package org.gradle.profiler; +import org.apache.commons.io.output.TeeOutputStream; + import java.io.*; public class Logging { diff --git a/src/main/java/org/gradle/profiler/TeeOutputStream.java b/src/main/java/org/gradle/profiler/TeeOutputStream.java deleted file mode 100644 index 5992ef5a..00000000 --- a/src/main/java/org/gradle/profiler/TeeOutputStream.java +++ /dev/null @@ -1,52 +0,0 @@ -package org.gradle.profiler; - -import java.io.IOException; -import java.io.OutputStream; -import java.util.Arrays; -import java.util.List; - -class TeeOutputStream extends OutputStream { - private final List targets; - - public TeeOutputStream(OutputStream... targets) { - this.targets = Arrays.asList(targets); - } - - @Override - public void write(int b) throws IOException { - withAll(stream -> stream.write(b)); - } - - @Override - public void write(byte[] b, int off, int len) throws IOException { - withAll(stream -> stream.write(b, off, len)); - } - - @Override - public void flush() throws IOException { - withAll(stream -> stream.flush()); - } - - @Override - public void close() throws IOException { - withAll(stream -> stream.close()); - } - - private void withAll(IOAction action) throws IOException { - IOException failure = null; - for (OutputStream target : targets) { - try { - action.withStream(target); - } catch (IOException e) { - failure = e; - } - } - if (failure != null) { - throw failure; - } - } - - private interface IOAction { - void withStream(OutputStream stream) throws IOException; - } -} diff --git a/src/test/groovy/org/gradle/profiler/AbstractIntegrationTest.groovy b/src/test/groovy/org/gradle/profiler/AbstractIntegrationTest.groovy index bbfbd6cb..053086d2 100644 --- a/src/test/groovy/org/gradle/profiler/AbstractIntegrationTest.groovy +++ b/src/test/groovy/org/gradle/profiler/AbstractIntegrationTest.groovy @@ -1,5 +1,6 @@ package org.gradle.profiler +import org.apache.commons.io.output.TeeOutputStream import spock.lang.Specification class AbstractIntegrationTest extends Specification { diff --git a/subprojects/ide-provisioning/build.gradle.kts b/subprojects/ide-provisioning/build.gradle.kts index 3c6b34c5..bc0633c9 100644 --- a/subprojects/ide-provisioning/build.gradle.kts +++ b/subprojects/ide-provisioning/build.gradle.kts @@ -11,19 +11,14 @@ java { repositories { maven { url = uri("https://www.jetbrains.com/intellij-repository/releases") } maven { url = uri("https://cache-redirector.jetbrains.com/intellij-dependencies") } - - maven { url = uri("https://cache-redirector.jetbrains.com/maven-central") } - maven { url = uri("https://www.jetbrains.com/intellij-repository/snapshots") } - maven { - url = uri("https://cache-redirector.jetbrains.com/packages.jetbrains.team/maven/p/grazi/grazie-platform-public") - } } dependencies { - implementation("com.jetbrains.intellij.tools:ide-starter-squashed:233.13135.103") { + implementation(libs.ideStarter) { exclude(group = "io.ktor") exclude(group = "com.jetbrains.infra") exclude(group = "com.jetbrains.intellij.remoteDev") } testImplementation(libs.bundles.testDependencies) + testImplementation(libs.commonIo) } diff --git a/subprojects/ide-provisioning/src/test/groovy/org/gradle/profiler/TeeOutputStream.java b/subprojects/ide-provisioning/src/test/groovy/org/gradle/profiler/TeeOutputStream.java deleted file mode 100644 index 11e5ebe4..00000000 --- a/subprojects/ide-provisioning/src/test/groovy/org/gradle/profiler/TeeOutputStream.java +++ /dev/null @@ -1,52 +0,0 @@ -package org.gradle.profiler; - -import java.io.IOException; -import java.io.OutputStream; -import java.util.Arrays; -import java.util.List; - -public class TeeOutputStream extends OutputStream { - private final List targets; - - public TeeOutputStream(OutputStream... targets) { - this.targets = Arrays.asList(targets); - } - - @Override - public void write(int b) throws IOException { - withAll(stream -> stream.write(b)); - } - - @Override - public void write(byte[] b, int off, int len) throws IOException { - withAll(stream -> stream.write(b, off, len)); - } - - @Override - public void flush() throws IOException { - withAll(stream -> stream.flush()); - } - - @Override - public void close() throws IOException { - withAll(stream -> stream.close()); - } - - private void withAll(IOAction action) throws IOException { - IOException failure = null; - for (OutputStream target : targets) { - try { - action.withStream(target); - } catch (IOException e) { - failure = e; - } - } - if (failure != null) { - throw failure; - } - } - - private interface IOAction { - void withStream(OutputStream stream) throws IOException; - } -} diff --git a/subprojects/ide-provisioning/src/test/groovy/org/gradle/profiler/ide/AbstractIdeProvisioningTest.groovy b/subprojects/ide-provisioning/src/test/groovy/org/gradle/profiler/ide/AbstractIdeProvisioningTest.groovy index 2f2b8a1f..66da88a7 100644 --- a/subprojects/ide-provisioning/src/test/groovy/org/gradle/profiler/ide/AbstractIdeProvisioningTest.groovy +++ b/subprojects/ide-provisioning/src/test/groovy/org/gradle/profiler/ide/AbstractIdeProvisioningTest.groovy @@ -1,6 +1,6 @@ package org.gradle.profiler.ide -import org.gradle.profiler.TeeOutputStream +import org.apache.commons.io.output.TeeOutputStream import org.junit.Rule import org.junit.rules.TemporaryFolder import spock.lang.Specification