First attempt
diff --git a/compiler/builtins-serializer/src/org/jetbrains/kotlin/serialization/builtins/FirBuiltInsSerializer.kt b/compiler/builtins-serializer/src/org/jetbrains/kotlin/serialization/builtins/FirBuiltInsSerializer.kt
new file mode 100644
index 0000000..8967381
--- /dev/null
+++ b/compiler/builtins-serializer/src/org/jetbrains/kotlin/serialization/builtins/FirBuiltInsSerializer.kt
@@ -0,0 +1,351 @@
+/*
+ * Copyright 2010-2020 JetBrains s.r.o. and Kotlin Programming Language contributors.
+ * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
+ */
+
+package org.jetbrains.kotlin.serialization.builtins
+
+import com.intellij.openapi.util.Disposer
+import com.intellij.openapi.vfs.StandardFileSystems
+import com.intellij.openapi.vfs.VirtualFileManager
+import org.jetbrains.kotlin.cli.common.*
+import org.jetbrains.kotlin.cli.common.config.addKotlinSourceRoots
+import org.jetbrains.kotlin.cli.common.fir.FirDiagnosticsCompilerResultsReporter
+import org.jetbrains.kotlin.cli.common.messages.*
+import org.jetbrains.kotlin.cli.common.messages.toLogger
+import org.jetbrains.kotlin.cli.jvm.compiler.*
+import org.jetbrains.kotlin.cli.jvm.compiler.pipeline.createContextForIncrementalCompilation
+import org.jetbrains.kotlin.cli.jvm.compiler.pipeline.createIncrementalCompilationScope
+import org.jetbrains.kotlin.cli.jvm.config.*
+import org.jetbrains.kotlin.cli.metadata.AbstractMetadataSerializer
+import org.jetbrains.kotlin.config.*
+import org.jetbrains.kotlin.diagnostics.DiagnosticReporterFactory
+import org.jetbrains.kotlin.fir.BinaryModuleData
+import org.jetbrains.kotlin.fir.DependencyListForCliModule
+import org.jetbrains.kotlin.fir.FirSession
+import org.jetbrains.kotlin.fir.declarations.FirClass
+import org.jetbrains.kotlin.fir.declarations.FirDeclaration
+import org.jetbrains.kotlin.fir.declarations.FirFile
+import org.jetbrains.kotlin.fir.declarations.utils.classId
+import org.jetbrains.kotlin.fir.extensions.FirExtensionRegistrar
+import org.jetbrains.kotlin.fir.packageFqName
+import org.jetbrains.kotlin.fir.pipeline.*
+import org.jetbrains.kotlin.fir.resolve.ScopeSession
+import org.jetbrains.kotlin.fir.serialization.*
+import org.jetbrains.kotlin.fir.serialization.constant.ConstValueProvider
+import org.jetbrains.kotlin.fir.symbols.SymbolInternals
+import org.jetbrains.kotlin.fir.symbols.impl.FirClassSymbol
+import org.jetbrains.kotlin.library.metadata.resolver.impl.KotlinResolvedLibraryImpl
+import org.jetbrains.kotlin.library.resolveSingleFileKlib
+import org.jetbrains.kotlin.metadata.ProtoBuf
+import org.jetbrains.kotlin.metadata.builtins.BuiltInsBinaryVersion
+import org.jetbrains.kotlin.metadata.deserialization.BinaryVersion
+import org.jetbrains.kotlin.metadata.jvm.serialization.JvmStringTable
+import org.jetbrains.kotlin.modules.TargetId
+import org.jetbrains.kotlin.name.ClassId
+import org.jetbrains.kotlin.name.FqName
+import org.jetbrains.kotlin.name.Name
+import org.jetbrains.kotlin.platform.CommonPlatforms
+import org.jetbrains.kotlin.serialization.SerializableStringTable
+import org.jetbrains.kotlin.serialization.deserialization.builtins.BuiltInSerializerProtocol
+import java.io.ByteArrayOutputStream
+import java.io.DataOutputStream
+import java.io.File
+
+class FirBuiltInsSerializer(
+    configuration: CompilerConfiguration,
+    environment: KotlinCoreEnvironment,
+) : AbstractMetadataSerializer<List<ModuleCompilerAnalyzedOutput>>(configuration, environment) {
+    companion object {
+        fun analyzeAndSerialize(
+            destDir: File,
+            srcDirs: List<File>,
+            extraClassPath: List<File>,
+            onComplete: (totalSize: Int, totalFiles: Int) -> Unit,
+        ) {
+            val rootDisposable = Disposer.newDisposable("Disposable for ${FirBuiltInsSerializer::class.simpleName}.analyzeAndSerialize")
+            val messageCollector = createMessageCollector()
+            val performanceManager = object : CommonCompilerPerformanceManager(presentableName = "test") {}
+            try {
+                val configuration = CompilerConfiguration().apply {
+                    this.messageCollector = messageCollector
+
+                    addKotlinSourceRoots(srcDirs.map { it.path })
+                    addJvmClasspathRoots(extraClassPath)
+                    configureJdkClasspathRoots()
+
+                    put(CLIConfigurationKeys.METADATA_DESTINATION_DIRECTORY, destDir)
+                    put(CommonConfigurationKeys.MODULE_NAME, "module for built-ins serialization")
+                    put(CLIConfigurationKeys.PERF_MANAGER, performanceManager)
+                }
+
+                val environment =
+                    KotlinCoreEnvironment.createForProduction(rootDisposable, configuration, EnvironmentConfigFiles.JVM_CONFIG_FILES)
+
+                val serializer = FirBuiltInsSerializer(configuration, environment)
+                serializer.analyzeAndSerialize()
+
+                onComplete(serializer.totalSize, serializer.totalFiles)
+            } finally {
+                messageCollector.flush()
+                Disposer.dispose(rootDisposable)
+            }
+        }
+
+        private fun createMessageCollector() = object : GroupingMessageCollector(
+            PrintingMessageCollector(System.err, MessageRenderer.PLAIN_RELATIVE_PATHS, false),
+            false,
+            false,
+        ) {
+            override fun report(severity: CompilerMessageSeverity, message: String, location: CompilerMessageSourceLocation?) {
+                // Only report diagnostics without a particular location because there's plenty of errors in built-in sources
+                // (functions without bodies, incorrect combination of modifiers, etc.)
+                if (location == null) {
+                    super.report(severity, message, location)
+                }
+            }
+        }
+    }
+
+    protected var totalSize = 0
+    protected var totalFiles = 0
+
+    override fun serialize(analysisResult: List<ModuleCompilerAnalyzedOutput>, destDir: File) {
+        destDir.deleteRecursively()
+        if (!destDir.mkdirs()) {
+            throw AssertionError("Could not make directories: " + destDir)
+        }
+
+        for (output in analysisResult) {
+            val (session, scopeSession, fir) = output
+
+            val languageVersionSettings = environment.configuration.languageVersionSettings
+            val firFile = fir.single()
+            val packageFragment = serializeSingleFirFile(
+                firFile,
+                session,
+                scopeSession,
+                actualizedExpectDeclarations = null,
+                object : FirSerializerExtensionBase(BuiltInSerializerProtocol) {
+                    override val session: FirSession
+                        get() = session
+                    override val scopeSession: ScopeSession
+                        get() = scopeSession
+                    override val metadataVersion: BinaryVersion
+                        get() = this@FirBuiltInsSerializer.metadataVersion
+                    override val constValueProvider: ConstValueProvider?
+                        get() = null
+                    override val additionalMetadataProvider: FirAdditionalMetadataProvider?
+                        get() = null
+                },
+                languageVersionSettings,
+            )
+            serializeBuiltInsFile(packageFragment, metadataVersion, destDir, firFile.packageFqName)
+        }
+    }
+
+    private fun serializeBuiltInsFile(proto: ProtoBuf.PackageFragment, version: BuiltInsBinaryVersion, destDir: File, fqName: FqName) {
+        val stream = ByteArrayOutputStream()
+        with(DataOutputStream(stream)) {
+            val versionArray = version.toArray()
+            writeInt(versionArray.size)
+            versionArray.forEach { writeInt(it) }
+        }
+        proto.writeTo(stream)
+        write(stream, destDir, fqName)
+    }
+
+    private fun write(stream: ByteArrayOutputStream, destDir: File, fqName: FqName) {
+        val destFile = File(destDir, BuiltInSerializerProtocol.getBuiltInsFilePath(fqName))
+        totalSize += stream.size()
+        totalFiles++
+        assert(!destFile.isDirectory) { "Cannot write because output destination is a directory: $destFile" }
+        destFile.parentFile.mkdirs()
+        destFile.writeBytes(stream.toByteArray())
+    }
+
+    private class FirJvmElementAwareStringTableForLightClasses : JvmStringTable(), FirElementAwareStringTable {
+        override fun getLocalClassIdReplacement(firClass: FirClass): ClassId {
+            return firClass.classId
+        }
+    }
+
+    override fun analyze(): List<ModuleCompilerAnalyzedOutput>? {
+        val performanceManager = environment.configuration.getNotNull(CLIConfigurationKeys.PERF_MANAGER)
+        performanceManager.notifyAnalysisStarted()
+
+        val configuration = environment.configuration
+        val messageCollector = configuration.getNotNull(CommonConfigurationKeys.MESSAGE_COLLECTOR_KEY)
+        val rootModuleName = Name.special("<${configuration.getNotNull(CommonConfigurationKeys.MODULE_NAME)}>")
+        val isLightTree = configuration.getBoolean(CommonConfigurationKeys.USE_LIGHT_TREE)
+
+        val binaryModuleData = BinaryModuleData.initialize(
+            rootModuleName,
+            CommonPlatforms.defaultCommonPlatform,
+        )
+        val libraryList = DependencyListForCliModule.build(binaryModuleData) {
+            val refinedPaths = configuration.get(K2MetadataConfigurationKeys.REFINES_PATHS)?.map { File(it) }.orEmpty()
+            dependencies(configuration.jvmClasspathRoots.filter { it !in refinedPaths }.map { it.toPath() })
+            dependencies(configuration.jvmModularRoots.map { it.toPath() })
+            friendDependencies(configuration[K2MetadataConfigurationKeys.FRIEND_PATHS] ?: emptyList())
+            dependsOnDependencies(refinedPaths.map { it.toPath() })
+        }
+
+        val diagnosticsReporter = DiagnosticReporterFactory.createPendingReporter()
+
+        val klibFiles = configuration.get(CLIConfigurationKeys.CONTENT_ROOTS).orEmpty()
+            .filterIsInstance<JvmClasspathRoot>()
+            .filter { it.file.isDirectory || it.file.extension == "klib" }
+            .map { it.file.absolutePath }
+
+        val logger = messageCollector.toLogger()
+
+        // TODO: This is a workaround for KT-63573. Revert it back when KT-64169 is fixed.
+//        val resolvedLibraries = CommonKLibResolver.resolve(klibFiles, logger).getFullResolvedList()
+        val resolvedLibraries =
+            klibFiles.map { KotlinResolvedLibraryImpl(resolveSingleFileKlib(org.jetbrains.kotlin.konan.file.File(it), logger)) }
+
+        val outputs = if (isLightTree) {
+            val projectEnvironment = environment.toAbstractProjectEnvironment() as VfsBasedProjectEnvironment
+            var librariesScope = projectEnvironment.getSearchScopeForProjectLibraries()
+            val groupedSources = collectSources(configuration, projectEnvironment, messageCollector)
+            val extensionRegistrars = FirExtensionRegistrar.getInstances(projectEnvironment.project)
+            val ltFiles = groupedSources.let { it.commonSources + it.platformSources }.toList()
+            val incrementalCompilationScope = createIncrementalCompilationScope(
+                configuration,
+                projectEnvironment,
+                incrementalExcludesScope = null
+            )?.also { librariesScope -= it }
+            val sessionsWithSources = prepareCommonSessions(
+                ltFiles, configuration, projectEnvironment, rootModuleName, extensionRegistrars, librariesScope,
+                libraryList, resolvedLibraries, groupedSources.isCommonSourceForLt, groupedSources.fileBelongsToModuleForLt,
+                createProviderAndScopeForIncrementalCompilation = { files ->
+                    createContextForIncrementalCompilation(
+                        configuration,
+                        projectEnvironment,
+                        projectEnvironment.getSearchScopeBySourceFiles(files),
+                        previousStepsSymbolProviders = emptyList(),
+                        incrementalCompilationScope
+                    )
+                }
+            )
+            sessionsWithSources.map { (session, files) ->
+                val firFiles = session.buildFirViaLightTree(files, diagnosticsReporter, performanceManager::addSourcesStats)
+                resolveAndCheckFir(session, firFiles, diagnosticsReporter)
+            }
+        } else {
+            val projectEnvironment = VfsBasedProjectEnvironment(
+                environment.project,
+                VirtualFileManager.getInstance().getFileSystem(StandardFileSystems.FILE_PROTOCOL)
+            ) { environment.createPackagePartProvider(it) }
+            var librariesScope = projectEnvironment.getSearchScopeForProjectLibraries()
+            val extensionRegistrars = FirExtensionRegistrar.getInstances(projectEnvironment.project)
+            val psiFiles = environment.getSourceFiles()
+            val sourceScope =
+                projectEnvironment.getSearchScopeByPsiFiles(psiFiles) + projectEnvironment.getSearchScopeForProjectJavaSources()
+            val providerAndScopeForIncrementalCompilation = createContextForIncrementalCompilation(
+                projectEnvironment,
+                configuration.get(JVMConfigurationKeys.INCREMENTAL_COMPILATION_COMPONENTS),
+                configuration,
+                configuration.get(JVMConfigurationKeys.MODULES)?.map(::TargetId),
+                sourceScope
+            )
+            providerAndScopeForIncrementalCompilation?.precompiledBinariesFileScope?.let {
+                librariesScope -= it
+            }
+            val sessionsWithSources = prepareCommonSessions(
+                psiFiles, configuration, projectEnvironment, rootModuleName, extensionRegistrars,
+                librariesScope, libraryList, resolvedLibraries, isCommonSourceForPsi, fileBelongsToModuleForPsi,
+                createProviderAndScopeForIncrementalCompilation = { providerAndScopeForIncrementalCompilation }
+            )
+
+            sessionsWithSources.map { (session, files) ->
+                val firFiles = session.buildFirFromKtFiles(files)
+                resolveAndCheckFir(session, firFiles, diagnosticsReporter)
+            }
+        }
+
+        outputs.runPlatformCheckers(diagnosticsReporter)
+
+        val renderDiagnosticNames = configuration.getBoolean(CLIConfigurationKeys.RENDER_DIAGNOSTIC_INTERNAL_NAME)
+        FirDiagnosticsCompilerResultsReporter.reportToMessageCollector(diagnosticsReporter, messageCollector, renderDiagnosticNames)
+
+        return if (diagnosticsReporter.hasErrors) {
+            null
+        } else {
+            outputs
+        }.also {
+            performanceManager.notifyAnalysisFinished()
+        }
+    }
+
+}
+
+@OptIn(SymbolInternals::class)
+fun serializeSingleFirFile(
+    file: FirFile, session: FirSession, scopeSession: ScopeSession,
+    actualizedExpectDeclarations: Set<FirDeclaration>?,
+    serializerExtension: FirSerializerExtension,
+    languageVersionSettings: LanguageVersionSettings,
+): ProtoBuf.PackageFragment {
+    val approximator = TypeApproximatorForMetadataSerializer(session)
+
+    val packageSerializer = FirElementSerializer.createTopLevel(
+        session, scopeSession, serializerExtension,
+        approximator,
+        languageVersionSettings,
+        produceHeaderKlib = false
+    )
+    val packageProto = packageSerializer.packagePartProto(file, actualizedExpectDeclarations).build()
+
+    val classesProto = mutableListOf<Pair<ProtoBuf.Class, Int>>()
+
+    fun FirClass.makeClassProtoWithNested() {
+        if (!isNotExpectOrShouldBeSerialized(actualizedExpectDeclarations) ||
+            !isNotPrivateOrShouldBeSerialized(produceHeaderKlib = false)
+        ) {
+            return
+        }
+
+        val classSerializer = FirElementSerializer.create(
+            session, scopeSession, klass = this, serializerExtension, parentSerializer = null,
+            approximator, languageVersionSettings, produceHeaderKlib = false
+        )
+        val index = classSerializer.stringTable.getFqNameIndex(this)
+
+        classesProto += classSerializer.classProto(this).build() to index
+
+        for (nestedClassifierSymbol in classSerializer.computeNestedClassifiersForClass(symbol)) {
+            (nestedClassifierSymbol as? FirClassSymbol<*>)?.fir?.makeClassProtoWithNested()
+        }
+    }
+
+    serializerExtension.processFile(file) {
+        for (declaration in file.declarations) {
+            (declaration as? FirClass)?.makeClassProtoWithNested()
+        }
+    }
+
+    return buildPackageFragment(
+        packageProto,
+        classesProto,
+        serializerExtension.stringTable as SerializableStringTable
+    )
+}
+
+fun buildPackageFragment(
+    packageProto: ProtoBuf.Package,
+    classesProto: List<Pair<ProtoBuf.Class, Int>>,
+    stringTable: SerializableStringTable,
+): ProtoBuf.PackageFragment {
+
+    val (stringTableProto, nameTableProto) = stringTable.buildProto()
+
+    return ProtoBuf.PackageFragment.newBuilder()
+        .setPackage(packageProto)
+        .addAllClass_(classesProto.map { it.first })
+        .setStrings(stringTableProto)
+        .setQualifiedNames(nameTableProto)
+        .build()
+}
+
diff --git a/compiler/fir/fir-serialization/src/org/jetbrains/kotlin/fir/serialization/FirSerializerExtension.kt b/compiler/fir/fir-serialization/src/org/jetbrains/kotlin/fir/serialization/FirSerializerExtension.kt
index dcb47e7..ecc2f69 100644
--- a/compiler/fir/fir-serialization/src/org/jetbrains/kotlin/fir/serialization/FirSerializerExtension.kt
+++ b/compiler/fir/fir-serialization/src/org/jetbrains/kotlin/fir/serialization/FirSerializerExtension.kt
@@ -34,7 +34,7 @@
     protected abstract val additionalMetadataProvider: FirAdditionalMetadataProvider?
 
     @OptIn(ConstValueProviderInternals::class)
-    internal inline fun <T> processFile(firFile: FirFile, crossinline action: () -> T): T {
+    inline fun <T> processFile(firFile: FirFile, crossinline action: () -> T): T {
         val previousFile = constValueProvider?.processingFirFile
         constValueProvider?.processingFirFile = firFile
         return try {
diff --git a/compiler/tests/org/jetbrains/kotlin/serialization/builtins/BuiltInsSerializerTest.kt b/compiler/tests/org/jetbrains/kotlin/serialization/builtins/BuiltInsSerializerTest.kt
index 1c03bb9..1db1c2b 100644
--- a/compiler/tests/org/jetbrains/kotlin/serialization/builtins/BuiltInsSerializerTest.kt
+++ b/compiler/tests/org/jetbrains/kotlin/serialization/builtins/BuiltInsSerializerTest.kt
@@ -33,12 +33,11 @@
 class BuiltInsSerializerTest : TestCaseWithTmpdir() {
     private fun doTest(fileName: String) {
         val source = "compiler/testData/serialization/builtinsSerializer/$fileName"
-        BuiltInsSerializer.analyzeAndSerialize(
+        FirBuiltInsSerializer.analyzeAndSerialize(
             tmpdir,
             srcDirs = listOf(File(source)),
             extraClassPath = listOf(ForTestCompileRuntime.runtimeJarForTests()),
-            dependOnOldBuiltIns = true,
-            onComplete = { _, _ -> }
+            onComplete = { _, _ -> },
         )
 
         val module = KotlinTestUtils.createEmptyModule("<module>", DefaultBuiltIns.Instance)