Introduce "-Xreport-output-files" to report source-output mapping for JPS
This makes "-verbose" not required for JPS to run correctly and therefore allows to print more useful debugging stuff in the compiler and read them in CLI, for example. The output will also be more readable because there'll be no "output" messages
This commit is contained in:
+3
@@ -84,6 +84,9 @@ public abstract class CommonCompilerArguments implements Serializable {
|
||||
@Argument(value = "-Xallow-kotlin-package", description = "Allow compiling code in package 'kotlin'")
|
||||
public boolean allowKotlinPackage;
|
||||
|
||||
@Argument(value = "-Xreport-output-files", description = "Report source to output files mapping")
|
||||
public boolean reportOutputFiles;
|
||||
|
||||
@Argument(value = "-Xplugin", valueDescription = "<path>", description = "Load plugins from the given classpath")
|
||||
public String[] pluginClasspaths;
|
||||
|
||||
|
||||
+1
-1
@@ -32,7 +32,7 @@ public enum CompilerMessageSeverity {
|
||||
OUTPUT;
|
||||
|
||||
public static final EnumSet<CompilerMessageSeverity> ERRORS = EnumSet.of(ERROR, EXCEPTION);
|
||||
public static final EnumSet<CompilerMessageSeverity> VERBOSE = EnumSet.of(OUTPUT, LOGGING);
|
||||
public static final EnumSet<CompilerMessageSeverity> VERBOSE = EnumSet.of(LOGGING);
|
||||
|
||||
public boolean isError() {
|
||||
return ERRORS.contains(this);
|
||||
|
||||
@@ -220,6 +220,9 @@ public abstract class CLICompiler<A extends CommonCompilerArguments> {
|
||||
if (arguments.intellijPluginRoot != null) {
|
||||
configuration.put(CLIConfigurationKeys.INTELLIJ_PLUGIN_ROOT, arguments.intellijPluginRoot);
|
||||
}
|
||||
if (arguments.reportOutputFiles) {
|
||||
configuration.put(CommonConfigurationKeys.REPORT_OUTPUT_FILES, true);
|
||||
}
|
||||
@SuppressWarnings("deprecation")
|
||||
CompilerJarLocator locator = services.get(CompilerJarLocator.class);
|
||||
if (locator != null) {
|
||||
|
||||
@@ -24,23 +24,22 @@ import org.jetbrains.kotlin.cli.common.messages.MessageCollector
|
||||
import org.jetbrains.kotlin.cli.common.messages.OutputMessageUtil
|
||||
import java.io.File
|
||||
|
||||
fun OutputFileCollection.writeAll(outputDir: File, report: (file: OutputFile, sources: List<File>, output: File) -> Unit) {
|
||||
fun OutputFileCollection.writeAll(outputDir: File, report: ((file: OutputFile, sources: List<File>, output: File) -> Unit)?) {
|
||||
for (file in asList()) {
|
||||
val sources = file.sourceFiles
|
||||
val output = File(outputDir, file.relativePath)
|
||||
report(file, sources, output)
|
||||
report?.invoke(file, sources, output)
|
||||
FileUtil.writeToFile(output, file.asByteArray())
|
||||
}
|
||||
}
|
||||
|
||||
private val REPORT_NOTHING: (OutputFile, List<File>, File) -> Unit = { _, _, _ -> }
|
||||
|
||||
fun OutputFileCollection.writeAllTo(outputDir: File) {
|
||||
writeAll(outputDir, REPORT_NOTHING)
|
||||
writeAll(outputDir, null)
|
||||
}
|
||||
|
||||
fun OutputFileCollection.writeAll(outputDir: File, messageCollector: MessageCollector) {
|
||||
writeAll(outputDir) { _, sources, output ->
|
||||
fun OutputFileCollection.writeAll(outputDir: File, messageCollector: MessageCollector, reportOutputFiles: Boolean) {
|
||||
if (!reportOutputFiles) writeAllTo(outputDir)
|
||||
else writeAll(outputDir) { _, sources, output ->
|
||||
messageCollector.report(CompilerMessageSeverity.OUTPUT, OutputMessageUtil.formatOutputMessage(sources, output))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -211,7 +211,8 @@ public class K2JSCompiler extends CLICompiler<K2JSCompilerArguments> {
|
||||
|
||||
ProgressIndicatorAndCompilationCanceledStatus.checkCanceled();
|
||||
|
||||
OutputUtilsKt.writeAll(outputFiles, outputDir, messageCollector);
|
||||
OutputUtilsKt.writeAll(outputFiles, outputDir, messageCollector,
|
||||
configuration.getBoolean(CommonConfigurationKeys.REPORT_OUTPUT_FILES));
|
||||
|
||||
return OK;
|
||||
}
|
||||
|
||||
+7
-4
@@ -46,6 +46,7 @@ import org.jetbrains.kotlin.codegen.GeneratedClassLoader
|
||||
import org.jetbrains.kotlin.codegen.KotlinCodegenFacade
|
||||
import org.jetbrains.kotlin.codegen.state.GenerationState
|
||||
import org.jetbrains.kotlin.codegen.state.GenerationStateEventCallback
|
||||
import org.jetbrains.kotlin.config.CommonConfigurationKeys
|
||||
import org.jetbrains.kotlin.config.CompilerConfiguration
|
||||
import org.jetbrains.kotlin.config.JVMConfigurationKeys
|
||||
import org.jetbrains.kotlin.config.addKotlinSourceRoots
|
||||
@@ -87,19 +88,21 @@ object KotlinToJVMBytecodeCompiler {
|
||||
outputFiles: OutputFileCollection,
|
||||
mainClass: FqName?
|
||||
) {
|
||||
val reportOutputFiles = configuration.getBoolean(CommonConfigurationKeys.REPORT_OUTPUT_FILES)
|
||||
val jarPath = configuration.get(JVMConfigurationKeys.OUTPUT_JAR)
|
||||
val messageCollector = configuration.get(CLIConfigurationKeys.MESSAGE_COLLECTOR_KEY, MessageCollector.NONE)
|
||||
if (jarPath != null) {
|
||||
val includeRuntime = configuration.get(JVMConfigurationKeys.INCLUDE_RUNTIME, false)
|
||||
CompileEnvironmentUtil.writeToJar(jarPath, includeRuntime, mainClass, outputFiles)
|
||||
messageCollector.report(
|
||||
OUTPUT, OutputMessageUtil.formatOutputMessage(outputFiles.asList().flatMap { it.sourceFiles }.distinct(), jarPath)
|
||||
)
|
||||
if (reportOutputFiles) {
|
||||
val message = OutputMessageUtil.formatOutputMessage(outputFiles.asList().flatMap { it.sourceFiles }.distinct(), jarPath)
|
||||
messageCollector.report(OUTPUT, message)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
val outputDir = configuration.get(JVMConfigurationKeys.OUTPUT_DIRECTORY) ?: File(".")
|
||||
outputFiles.writeAll(outputDir, messageCollector)
|
||||
outputFiles.writeAll(outputDir, messageCollector, reportOutputFiles)
|
||||
}
|
||||
|
||||
private fun createOutputFilesFlushingCallbackIfPossible(configuration: CompilerConfiguration): GenerationStateEventCallback {
|
||||
|
||||
@@ -25,6 +25,9 @@ object CommonConfigurationKeys {
|
||||
|
||||
@JvmField
|
||||
val MODULE_NAME = CompilerConfigurationKey<String>("module name")
|
||||
|
||||
@JvmField
|
||||
val REPORT_OUTPUT_FILES = CompilerConfigurationKey<Boolean>("report output files")
|
||||
}
|
||||
|
||||
var CompilerConfiguration.languageVersionSettings: LanguageVersionSettings
|
||||
|
||||
+1
@@ -424,6 +424,7 @@ class IncrementalJvmCompilerRunner(
|
||||
val destination = args.destination
|
||||
args.destination = null
|
||||
args.module = moduleFile.absolutePath
|
||||
args.reportOutputFiles = true
|
||||
val outputItemCollector = OutputItemsCollectorImpl()
|
||||
@Suppress("NAME_SHADOWING")
|
||||
val messageCollector = MessageCollectorWrapper(messageCollector, outputItemCollector)
|
||||
|
||||
+1
@@ -5,6 +5,7 @@ where advanced options include:
|
||||
-Xrepeat=<count> Repeat compilation (for performance analysis)
|
||||
-Xskip-metadata-version-check Load classes with bad metadata version anyway (incl. pre-release classes)
|
||||
-Xallow-kotlin-package Allow compiling code in package 'kotlin'
|
||||
-Xreport-output-files Report source to output files mapping
|
||||
-Xplugin=<path> Load plugins from the given classpath
|
||||
-Xmulti-platform Enable experimental language support for multi-platform projects
|
||||
-Xno-check-impl Do not check presence of 'impl' modifier in multi-platform projects
|
||||
|
||||
+1
@@ -18,6 +18,7 @@ where advanced options include:
|
||||
-Xrepeat=<count> Repeat compilation (for performance analysis)
|
||||
-Xskip-metadata-version-check Load classes with bad metadata version anyway (incl. pre-release classes)
|
||||
-Xallow-kotlin-package Allow compiling code in package 'kotlin'
|
||||
-Xreport-output-files Report source to output files mapping
|
||||
-Xplugin=<path> Load plugins from the given classpath
|
||||
-Xmulti-platform Enable experimental language support for multi-platform projects
|
||||
-Xno-check-impl Do not check presence of 'impl' modifier in multi-platform projects
|
||||
|
||||
@@ -4,10 +4,6 @@ Buildfile: [TestData]/build.xml
|
||||
build:
|
||||
[kotlin2js] Compiling [[TestData]/root1] => [[Temp]/out.js]
|
||||
[kotlin2js] logging: compiling source files: [TestData]/root1/foo.kt
|
||||
[kotlin2js] output: output:
|
||||
[kotlin2js] [Temp]/out.js
|
||||
[kotlin2js] Sources:
|
||||
[kotlin2js] [TestData]/root1/foo.kt
|
||||
|
||||
BUILD SUCCESSFUL
|
||||
Total time: [time]
|
||||
|
||||
@@ -5,10 +5,6 @@ build:
|
||||
[kotlinc] Compiling [[TestData]/hello.kt] => [[Temp]/hello.jar]
|
||||
[kotlinc] logging: using Kotlin home directory [KotlinProjectHome]/dist/kotlinc
|
||||
[kotlinc] logging: configuring the compilation environment
|
||||
[kotlinc] output: output:
|
||||
[kotlinc] [Temp]/hello.jar
|
||||
[kotlinc] Sources:
|
||||
[kotlinc] [TestData]/hello.kt
|
||||
|
||||
BUILD SUCCESSFUL
|
||||
Total time: [time]
|
||||
|
||||
@@ -29,7 +29,6 @@ import org.jetbrains.kotlin.daemon.common.*
|
||||
import org.jetbrains.kotlin.integration.KotlinIntegrationTestBase
|
||||
import org.jetbrains.kotlin.scripts.captureOut
|
||||
import org.jetbrains.kotlin.test.KotlinTestUtils
|
||||
import org.jetbrains.kotlin.utils.keysToMap
|
||||
import org.junit.Assert
|
||||
import java.io.File
|
||||
import java.net.URLClassLoader
|
||||
@@ -110,7 +109,8 @@ class CompilerApiTest : KotlinIntegrationTestBase() {
|
||||
fun testHelloAppLocal() {
|
||||
val messageCollector = TestMessageCollector()
|
||||
val jar = tmpdir.absolutePath + File.separator + "hello.jar"
|
||||
val (code, outputs) = compileLocally(messageCollector, "-include-runtime", File(getHelloAppBaseDir(), "hello.kt").absolutePath, "-d", jar)
|
||||
val (code, outputs) = compileLocally(messageCollector, "-include-runtime", File(getHelloAppBaseDir(), "hello.kt").absolutePath,
|
||||
"-d", jar, "-Xreport-output-files")
|
||||
Assert.assertEquals(0, code)
|
||||
Assert.assertTrue(outputs.isNotEmpty())
|
||||
Assert.assertEquals(jar, outputs.first().outputFile?.absolutePath)
|
||||
@@ -129,12 +129,13 @@ class CompilerApiTest : KotlinIntegrationTestBase() {
|
||||
|
||||
val daemonJVMOptions = configureDaemonJVMOptions("D$COMPILE_DAEMON_LOG_PATH_PROPERTY=\"${logFile.loggerCompatiblePath}\"",
|
||||
inheritMemoryLimits = false, inheritAdditionalProperties = false)
|
||||
val messageCollector = TestMessageCollector()
|
||||
val jar = tmpdir.absolutePath + File.separator + "hello.jar"
|
||||
|
||||
try {
|
||||
val (code, outputs) = compileOnDaemon(flagFile, compilerId, daemonJVMOptions, daemonOptions, messageCollector,
|
||||
"-include-runtime", File(getHelloAppBaseDir(), "hello.kt").absolutePath, "-d", jar)
|
||||
val (code, outputs) = compileOnDaemon(
|
||||
flagFile, compilerId, daemonJVMOptions, daemonOptions, TestMessageCollector(), "-include-runtime",
|
||||
File(getHelloAppBaseDir(), "hello.kt").absolutePath, "-d", jar, "-Xreport-output-files"
|
||||
)
|
||||
Assert.assertEquals(0, code)
|
||||
Assert.assertTrue(outputs.isNotEmpty())
|
||||
Assert.assertEquals(jar, outputs.first().outputFile?.absolutePath)
|
||||
@@ -149,7 +150,8 @@ class CompilerApiTest : KotlinIntegrationTestBase() {
|
||||
|
||||
fun testSimpleScriptLocal() {
|
||||
val messageCollector = TestMessageCollector()
|
||||
val (code, outputs) = compileLocally(messageCollector, File(getSimpleScriptBaseDir(), "script.kts").absolutePath, "-d", tmpdir.absolutePath)
|
||||
val (code, outputs) = compileLocally(messageCollector, File(getSimpleScriptBaseDir(), "script.kts").absolutePath,
|
||||
"-d", tmpdir.absolutePath, "-Xreport-output-files")
|
||||
Assert.assertEquals(0, code)
|
||||
Assert.assertTrue(outputs.isNotEmpty())
|
||||
Assert.assertEquals(File(tmpdir, "Script.class").absolutePath, outputs.first().outputFile?.absolutePath)
|
||||
@@ -168,12 +170,11 @@ class CompilerApiTest : KotlinIntegrationTestBase() {
|
||||
|
||||
val daemonJVMOptions = configureDaemonJVMOptions("D$COMPILE_DAEMON_LOG_PATH_PROPERTY=\"${logFile.loggerCompatiblePath}\"",
|
||||
inheritMemoryLimits = false, inheritAdditionalProperties = false)
|
||||
val messageCollector = TestMessageCollector()
|
||||
val jar = tmpdir.absolutePath + File.separator + "hello.jar"
|
||||
|
||||
try {
|
||||
val (code, outputs) = compileOnDaemon(flagFile, compilerId, daemonJVMOptions, daemonOptions, messageCollector,
|
||||
File(getSimpleScriptBaseDir(), "script.kts").absolutePath, "-d", tmpdir.absolutePath)
|
||||
val (code, outputs) = compileOnDaemon(
|
||||
flagFile, compilerId, daemonJVMOptions, daemonOptions, TestMessageCollector(),
|
||||
File(getSimpleScriptBaseDir(), "script.kts").absolutePath, "-Xreport-output-files", "-d", tmpdir.absolutePath
|
||||
)
|
||||
Assert.assertEquals(0, code)
|
||||
Assert.assertTrue(outputs.isNotEmpty())
|
||||
Assert.assertEquals(File(tmpdir, "Script.class").absolutePath, outputs.first().outputFile?.absolutePath)
|
||||
|
||||
@@ -247,8 +247,9 @@ class KotlinBuilder : ModuleLevelBuilder(BuilderCategory.SOURCE_PROCESSOR) {
|
||||
return ABORT
|
||||
}
|
||||
|
||||
val commonArguments = compilerArgumentsForChunk(chunk)
|
||||
commonArguments.verbose = true // Make compiler report source to output files mapping
|
||||
val commonArguments = compilerArgumentsForChunk(chunk).apply {
|
||||
reportOutputFiles = true
|
||||
}
|
||||
|
||||
val allCompiledFiles = getAllCompiledFilesContainer(context)
|
||||
val filesToCompile = KotlinSourceFileCollector.getDirtySourceFiles(dirtyFilesHolder)
|
||||
|
||||
+4
-3
@@ -24,6 +24,7 @@ import org.jetbrains.kotlin.compiler.plugin.CliOption
|
||||
import org.jetbrains.kotlin.compiler.plugin.CliOptionProcessingException
|
||||
import org.jetbrains.kotlin.compiler.plugin.CommandLineProcessor
|
||||
import org.jetbrains.kotlin.compiler.plugin.ComponentRegistrar
|
||||
import org.jetbrains.kotlin.config.CommonConfigurationKeys
|
||||
import org.jetbrains.kotlin.config.CompilerConfiguration
|
||||
import org.jetbrains.kotlin.config.CompilerConfigurationKey
|
||||
import org.jetbrains.kotlin.resolve.jvm.extensions.AnalysisHandlerExtension
|
||||
@@ -89,10 +90,10 @@ class AnnotationCollectorComponentRegistrar : ComponentRegistrar {
|
||||
}
|
||||
|
||||
val stubs = configuration.get(AnnotationCollectorConfigurationKeys.STUBS_PATH)
|
||||
val messageCollector = configuration.get(CLIConfigurationKeys.MESSAGE_COLLECTOR_KEY, MessageCollector.NONE)
|
||||
if (stubs != null) {
|
||||
AnalysisHandlerExtension.registerExtension(project, StubProducerExtension(File(stubs), messageCollector))
|
||||
val messageCollector = configuration.get(CLIConfigurationKeys.MESSAGE_COLLECTOR_KEY, MessageCollector.NONE)
|
||||
val reportOutputFiles = configuration.getBoolean(CommonConfigurationKeys.REPORT_OUTPUT_FILES)
|
||||
AnalysisHandlerExtension.registerExtension(project, StubProducerExtension(File(stubs), messageCollector, reportOutputFiles))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+6
-2
@@ -32,7 +32,11 @@ import org.jetbrains.kotlin.resolve.jvm.extensions.AnalysisHandlerExtension
|
||||
import org.jetbrains.org.objectweb.asm.ClassWriter
|
||||
import java.io.File
|
||||
|
||||
class StubProducerExtension(val stubsOutputDir: File, val messageCollector: MessageCollector) : AnalysisHandlerExtension {
|
||||
class StubProducerExtension(
|
||||
private val stubsOutputDir: File,
|
||||
private val messageCollector: MessageCollector,
|
||||
private val reportOutputFiles: Boolean
|
||||
) : AnalysisHandlerExtension {
|
||||
override fun analysisCompleted(
|
||||
project: Project,
|
||||
module: ModuleDescriptor,
|
||||
@@ -50,7 +54,7 @@ class StubProducerExtension(val stubsOutputDir: File, val messageCollector: Mess
|
||||
KotlinCodegenFacade.compileCorrectFiles(generationState, CompilationErrorHandler.THROW_EXCEPTION)
|
||||
|
||||
if (!stubsOutputDir.exists()) stubsOutputDir.mkdirs()
|
||||
generationState.factory.writeAll(stubsOutputDir, messageCollector)
|
||||
generationState.factory.writeAll(stubsOutputDir, messageCollector, reportOutputFiles)
|
||||
|
||||
generationState.destroy()
|
||||
return AnalysisResult.success(BindingContext.EMPTY, module, shouldGenerateCode = false)
|
||||
|
||||
@@ -19,13 +19,15 @@ package org.jetbrains.kotlin.kapt3
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.sun.tools.javac.tree.JCTree
|
||||
import org.jetbrains.kotlin.analyzer.AnalysisResult
|
||||
import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSeverity
|
||||
import org.jetbrains.kotlin.backend.common.output.OutputFile
|
||||
import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSeverity.OUTPUT
|
||||
import org.jetbrains.kotlin.cli.common.messages.MessageCollector
|
||||
import org.jetbrains.kotlin.cli.common.messages.OutputMessageUtil
|
||||
import org.jetbrains.kotlin.cli.common.output.outputUtils.writeAll
|
||||
import org.jetbrains.kotlin.codegen.CompilationErrorHandler
|
||||
import org.jetbrains.kotlin.codegen.KotlinCodegenFacade
|
||||
import org.jetbrains.kotlin.codegen.state.GenerationState
|
||||
import org.jetbrains.kotlin.config.CommonConfigurationKeys
|
||||
import org.jetbrains.kotlin.config.CompilerConfiguration
|
||||
import org.jetbrains.kotlin.descriptors.ModuleDescriptor
|
||||
import org.jetbrains.kotlin.kapt3.diagnostic.ErrorsKapt3
|
||||
@@ -250,17 +252,21 @@ abstract class AbstractKapt3Extension(
|
||||
converter: ClassFileToSourceStubConverter) {
|
||||
val incrementalDataOutputDir = this.incrementalDataOutputDir ?: return
|
||||
|
||||
generationState.factory.writeAll(incrementalDataOutputDir) { file, sources, output ->
|
||||
val stubFileObject = converter.bindings[file.relativePath.substringBeforeLast(".class", missingDelimiterValue = "")]
|
||||
if (stubFileObject != null) {
|
||||
val stubFile = File(stubsOutputDir, stubFileObject.name)
|
||||
if (stubFile.exists()) {
|
||||
messageCollector.report(CompilerMessageSeverity.OUTPUT, OutputMessageUtil.formatOutputMessage(sources, stubFile))
|
||||
}
|
||||
}
|
||||
val reportOutputFiles = generationState.configuration.getBoolean(CommonConfigurationKeys.REPORT_OUTPUT_FILES)
|
||||
generationState.factory.writeAll(
|
||||
incrementalDataOutputDir,
|
||||
if (!reportOutputFiles) null else fun(file: OutputFile, sources: List<File>, output: File) {
|
||||
val stubFileObject = converter.bindings[file.relativePath.substringBeforeLast(".class", missingDelimiterValue = "")]
|
||||
if (stubFileObject != null) {
|
||||
val stubFile = File(stubsOutputDir, stubFileObject.name)
|
||||
if (stubFile.exists()) {
|
||||
messageCollector.report(OUTPUT, OutputMessageUtil.formatOutputMessage(sources, stubFile))
|
||||
}
|
||||
}
|
||||
|
||||
messageCollector.report(CompilerMessageSeverity.OUTPUT, OutputMessageUtil.formatOutputMessage(sources, output))
|
||||
}
|
||||
messageCollector.report(OUTPUT, OutputMessageUtil.formatOutputMessage(sources, output))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
protected abstract fun loadProcessors(): List<Processor>
|
||||
|
||||
+1
-1
@@ -62,7 +62,7 @@ abstract class AbstractAnnotationProcessorBoxTest : KotlinTestWithEnvironment()
|
||||
|
||||
if (supportStubs) {
|
||||
val stubsDir = KotlinTestUtils.tmpDir("class-stubs")
|
||||
val stubProducerExtension = StubProducerExtension(stubsDir, MessageCollector.NONE)
|
||||
val stubProducerExtension = StubProducerExtension(stubsDir, MessageCollector.NONE, false)
|
||||
AnalysisHandlerExtension.registerExtension(project, stubProducerExtension)
|
||||
}
|
||||
|
||||
|
||||
+2
-1
@@ -208,7 +208,8 @@ class SourceSectionsTest : TestCaseWithTmpdir() {
|
||||
sourceToOutput.forEach { (source, expectedOutput) ->
|
||||
val args = arrayOf(source.canonicalPath, "-d", tmpdir.canonicalPath,
|
||||
"-Xplugin=${sourceSectionsPluginJar.canonicalPath}",
|
||||
"-P", TEST_ALLOWED_SECTIONS.joinToString(",") { "plugin:${SourceSectionsCommandLineProcessor.PLUGIN_ID}:${SourceSectionsCommandLineProcessor.SECTIONS_OPTION.name}=$it" })
|
||||
"-P", TEST_ALLOWED_SECTIONS.joinToString(",") { "plugin:${SourceSectionsCommandLineProcessor.PLUGIN_ID}:${SourceSectionsCommandLineProcessor.SECTIONS_OPTION.name}=$it" },
|
||||
"-Xreport-output-files")
|
||||
|
||||
messageCollector.clear()
|
||||
val outputs = arrayListOf<OutputMessageUtil.Output>()
|
||||
|
||||
Reference in New Issue
Block a user