Moving PackageRepository.Default into its own EnsoPackageRepository (#7395)

This commit is contained in:
Jaroslav Tulach 2023-07-26 09:59:56 +02:00 committed by GitHub
parent 4b5a2e2176
commit f2c46428bd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 670 additions and 651 deletions

View File

@ -1,28 +1,30 @@
package org.enso.compiler; package org.enso.compiler;
import buildinfo.Info;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.oracle.truffle.api.TruffleFile;
import com.oracle.truffle.api.TruffleLogger;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.ObjectInputStream; import java.io.ObjectInputStream;
import java.io.ObjectOutputStream; import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.List;
import java.util.Optional;
import java.util.logging.Level;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.enso.compiler.data.BindingsMap; import org.enso.compiler.data.BindingsMap;
import org.enso.editions.LibraryName; import org.enso.editions.LibraryName;
import org.enso.interpreter.runtime.EnsoContext; import org.enso.interpreter.runtime.EnsoContext;
import org.enso.pkg.QualifiedName; import org.enso.pkg.QualifiedName;
import org.enso.pkg.SourceFile; import org.enso.pkg.SourceFile;
import scala.collection.immutable.Map;
import java.io.Serializable; import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.List; import com.fasterxml.jackson.core.JsonProcessingException;
import java.util.Optional; import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.logging.Level; import com.oracle.truffle.api.TruffleFile;
import com.oracle.truffle.api.TruffleLogger;
import buildinfo.Info;
import scala.collection.immutable.Map;
public final class ImportExportCache extends Cache<ImportExportCache.CachedBindings, ImportExportCache.Metadata> { public final class ImportExportCache extends Cache<ImportExportCache.CachedBindings, ImportExportCache.Metadata> {
@ -125,11 +127,12 @@ public final class ImportExportCache extends Cache<ImportExportCache.CachedBindi
} }
// CachedBindings is not a record **on purpose**. There appears to be a Frgaal bug leading to invalid compilation error. // CachedBindings is not a record **on purpose**. There appears to be a Frgaal bug leading to invalid compilation error.
static class CachedBindings { public static final class CachedBindings {
private final LibraryName _libraryName; private final LibraryName _libraryName;
private final MapToBindings _bindings; private final MapToBindings _bindings;
private final Optional<List<SourceFile<TruffleFile>>> _sources; private final Optional<List<SourceFile<TruffleFile>>> _sources;
public CachedBindings(LibraryName libraryName, MapToBindings bindings, Optional<List<SourceFile<TruffleFile>>> sources) {
CachedBindings(LibraryName libraryName, MapToBindings bindings, Optional<List<SourceFile<TruffleFile>>> sources) {
this._libraryName = libraryName; this._libraryName = libraryName;
this._bindings = bindings; this._bindings = bindings;
this._sources = sources; this._sources = sources;

View File

@ -156,7 +156,7 @@ public class EnsoContext {
var resourceManager = new org.enso.distribution.locking.ResourceManager(lockManager); var resourceManager = new org.enso.distribution.locking.ResourceManager(lockManager);
packageRepository = packageRepository =
PackageRepository.initializeRepository( DefaultPackageRepository.initializeRepository(
OptionConverters.toScala(projectPackage), OptionConverters.toScala(projectPackage),
OptionConverters.toScala(languageHome), OptionConverters.toScala(languageHome),
OptionConverters.toScala(editionOverride), OptionConverters.toScala(editionOverride),

View File

@ -1,41 +1,12 @@
package org.enso.compiler package org.enso.compiler
import com.oracle.truffle.api.TruffleFile import com.oracle.truffle.api.TruffleFile
import com.typesafe.scalalogging.Logger import org.enso.editions.LibraryName
import org.apache.commons.lang3.StringUtils import org.enso.interpreter.runtime.Module
import org.enso.distribution.locking.ResourceManager import org.enso.pkg.{ComponentGroups, Package}
import org.enso.distribution.{DistributionManager, LanguageHome}
import org.enso.editions.updater.EditionManager
import org.enso.editions.{DefaultEdition, Editions, LibraryName, LibraryVersion}
import org.enso.interpreter.instrument.NotificationHandler
import org.enso.interpreter.runtime.builtin.Builtins
import org.enso.interpreter.runtime.util.TruffleFileSystem
import org.enso.interpreter.runtime.{EnsoContext, Module}
import org.enso.librarymanager.published.repository.LibraryManifest
import org.enso.librarymanager.resolved.LibraryRoot
import org.enso.librarymanager.{
DefaultLibraryProvider,
ResolvingLibraryProvider
}
import org.enso.logger.masking.MaskedPath
import org.enso.pkg.{
Component,
ComponentGroup,
ComponentGroups,
ExtendedComponentGroup,
Package,
PackageManager,
QualifiedName,
SourceFile
}
import org.enso.text.buffer.Rope
import org.enso.polyglot.CompilationStage
import java.nio.file.Path
import scala.collection.immutable.ListSet import scala.collection.immutable.ListSet
import scala.jdk.OptionConverters.RichOption import scala.jdk.OptionConverters.RichOption
import scala.jdk.CollectionConverters.{IterableHasAsJava, SeqHasAsJava}
import scala.util.{Failure, Try, Using}
/** Manages loaded packages and modules. */ /** Manages loaded packages and modules. */
trait PackageRepository { trait PackageRepository {
@ -181,609 +152,4 @@ object PackageRepository {
} }
} }
/** The default [[PackageRepository]] implementation.
*
* @param libraryProvider the [[ResolvingLibraryProvider]] which resolves
* which library version should be imported and
* locates them (or downloads if they are missing)
* @param context the language context
* @param builtins the builtins module
* @param notificationHandler a notification handler
*/
class Default(
libraryProvider: ResolvingLibraryProvider,
context: EnsoContext,
builtins: Builtins,
notificationHandler: NotificationHandler
) extends PackageRepository {
private val logger = Logger[Default]
implicit private val fs: TruffleFileSystem = new TruffleFileSystem
private val packageManager = new PackageManager[TruffleFile]
private var projectPackage: Option[Package[TruffleFile]] = None
/** The mapping containing all loaded packages.
*
* It should be modified only from within synchronized sections, but it may
* be always read. Thus elements should be added to this mapping only after
* all library loading bookkeeping has been finished - so that if other,
* unsynchronized threads read this map, every element it contains is
* already fully processed.
*/
private val loadedPackages
: collection.mutable.Map[LibraryName, Option[Package[TruffleFile]]] = {
val builtinsName = LibraryName(Builtins.NAMESPACE, Builtins.PACKAGE_NAME)
collection.mutable.LinkedHashMap(builtinsName -> None)
}
/** The mapping containing loaded modules.
*
* We use [[String]] as the key as we often index into this map based on
* qualified names that come from interop (via
* [[org.enso.interpreter.runtime.scope.TopLevelScope]]). These arrive as
* Strings, and constantly converting them into [[QualifiedName]]s would
* add more overhead than is probably necessary.
*/
private val loadedModules: collection.concurrent.Map[String, Module] =
collection.concurrent.TrieMap(Builtins.MODULE_NAME -> builtins.getModule)
/** The mapping containing loaded component groups.
*
* It should be modified and read only from within synchronized sections.
* The component mapping is added to the collection after ensuring that the
* corresponding library was loaded.
*/
private val loadedComponents
: collection.mutable.Map[LibraryName, ComponentGroups] = {
val builtinsName = LibraryName(Builtins.NAMESPACE, Builtins.PACKAGE_NAME)
collection.mutable.LinkedHashMap(builtinsName -> ComponentGroups.empty)
}
/** The mapping between the library and its cached bindings, if already laoded. */
private val loadedLibraryBindings: collection.mutable.Map[
LibraryName,
ImportExportCache.CachedBindings
] =
collection.mutable.LinkedHashMap()
private def getComponentModules: ListSet[Module] = {
val modules = for {
componentGroups <- loadedComponents.values
newComponents = componentGroups.newGroups.flatMap(_.exports)
extendedComponents = componentGroups.extendedGroups.flatMap(_.exports)
component <- newComponents ++ extendedComponents
module <- findComponentModule(component)
} yield module
modules.to(ListSet)
}
private def findComponentModule(component: Component): Option[Module] = {
def mkModuleName(path: Array[String]): String =
path.mkString(LibraryName.separator.toString)
@scala.annotation.tailrec
def go(path: Array[String]): Option[Module] =
if (path.isEmpty) None
else {
loadedModules.get(mkModuleName(path)) match {
case Some(module) => Some(module)
case None => go(path.init)
}
}
go(component.name.split(LibraryName.separator))
}
/** @inheritdoc */
override def getModuleMap: ModuleMap = loadedModules
/** @inheritdoc */
override def freezeModuleMap: FrozenModuleMap = loadedModules.toMap
/** @inheritdoc */
override def getComponents: ComponentsMap =
this.synchronized {
loadedComponents.toMap
}
/** @inheritdoc */
override def getPendingModules: ListSet[Module] =
this.synchronized {
for {
module <- getComponentModules
isCompiled =
module.getCompilationStage.isAtLeast(
CompilationStage.AFTER_CODEGEN
)
if !isCompiled
} yield module
}
/** @inheritdoc */
override def registerMainProjectPackage(
libraryName: LibraryName,
pkg: Package[TruffleFile]
): Unit = {
projectPackage = Some(pkg)
registerPackageInternal(
libraryName = libraryName,
pkg = pkg,
libraryVersion = LibraryVersion.Local,
isLibrary = false
)
}
/** @inheritdoc */
override def getMainProjectPackage: Option[Package[TruffleFile]] = {
projectPackage
}
private def registerPackageInternal(
libraryName: LibraryName,
libraryVersion: LibraryVersion,
pkg: Package[TruffleFile],
isLibrary: Boolean
): Unit = {
val extensions = pkg.listPolyglotExtensions("java")
extensions.foreach(context.getEnvironment.addToHostClassPath)
val (regularModules, syntheticModulesMetadata) = pkg
.listSources()
.map(srcFile =>
(
new Module(srcFile.qualifiedName, pkg, srcFile.file),
inferSyntheticModules(srcFile)
)
)
.unzip
regularModules.foreach(registerModule)
syntheticModulesMetadata.flatten
.groupMap(_._1)(v => (v._2, v._3))
.foreach { case (qName, modulesWithSources) =>
val source = modulesWithSources
.map(_._2)
.foldLeft("")(_ ++ "\n" ++ _)
registerSyntheticModule(
Module.synthetic(
qName,
pkg,
Rope(source)
),
modulesWithSources.map(_._1)
)
}
if (isLibrary) {
val root = Path.of(pkg.root.toString)
notificationHandler.addedLibrary(libraryName, libraryVersion, root)
}
loadedPackages.put(libraryName, Some(pkg))
}
/** For any given source file, infer data necessary to generate synthetic modules as well as their contents.
* E.g., for A/B/C.enso it infers modules
* - A.B that exports A.B.C
* - A that exports A.B
*
* @param srcFile Enso source file to consider
* @return a list of triples representing the name of submodule along the path, what submodule it exports and its contents
*/
private def inferSyntheticModules(
srcFile: SourceFile[TruffleFile]
): List[(QualifiedName, QualifiedName, String)] = {
def listAllIntermediateModules(
namespace: String,
name: String,
elements: List[String],
exportItem: String
): List[(QualifiedName, QualifiedName, String)] = {
elements match {
case Nil =>
Nil
case lastModuleName :: parts =>
val pathElems = elements.reverse
val modName =
s"${namespace}.$name.${pathElems.mkString(".")}.$exportItem"
val modSource =
s"""|import $modName
|export $modName
|""".stripMargin
val syntheticModuleInfo = (
QualifiedName(namespace :: name :: parts.reverse, lastModuleName),
QualifiedName(namespace :: name :: pathElems, exportItem),
modSource
)
syntheticModuleInfo :: listAllIntermediateModules(
namespace,
name,
parts,
lastModuleName
)
}
}
srcFile.qualifiedName.path match {
case namespace :: name :: rest =>
listAllIntermediateModules(
namespace,
name,
rest.reverse,
srcFile.qualifiedName.item
)
case _ =>
Nil
}
}
/** This package modifies the [[loadedPackages]], so it should be only
* called from within synchronized sections.
*/
private def loadPackage(
libraryName: LibraryName,
libraryVersion: LibraryVersion,
root: LibraryRoot
): Either[Error, Package[TruffleFile]] = Try {
logger.debug(
s"Loading library $libraryName from " +
s"[${MaskedPath(root.location).applyMasking()}]."
)
val rootFile = context.getEnvironment.getInternalTruffleFile(
root.location.toAbsolutePath.normalize.toString
)
val pkg = packageManager.loadPackage(rootFile).get
registerPackageInternal(
libraryName = libraryName,
libraryVersion = libraryVersion,
pkg = pkg,
isLibrary = true
)
pkg
}.toEither.left.map { error => Error.PackageLoadingError(error.getMessage) }
/** @inheritdoc */
override def initialize(): Either[Error, Unit] =
this.synchronized {
val unprocessedPackages =
loadedPackages.keySet
.diff(loadedComponents.keySet)
.flatMap(loadedPackages(_))
unprocessedPackages.foldLeft[Either[Error, Unit]](Right(())) {
(accumulator, pkg) =>
for {
_ <- accumulator
_ <- resolveComponentGroups(pkg)
} yield ()
}
}
private def resolveComponentGroups(
pkg: Package[TruffleFile]
): Either[Error, Unit] =
if (loadedComponents.contains(pkg.libraryName)) Right(())
else {
pkg.getConfig().componentGroups match {
case Left(err) =>
Left(Error.PackageLoadingError(err.getMessage()))
case Right(componentGroups) =>
logger.debug(
s"Resolving component groups of package [${pkg.name}]."
)
registerComponentGroups(pkg.libraryName, componentGroups.newGroups)
componentGroups.extendedGroups
.foldLeft[Either[Error, Unit]](Right(())) {
(accumulator, componentGroup) =>
for {
_ <- accumulator
extendedLibraryName = componentGroup.group.libraryName
_ <- ensurePackageIsLoaded(extendedLibraryName)
pkgOpt = loadedPackages(extendedLibraryName)
_ <- pkgOpt.fold[Either[Error, Unit]](Right(()))(
resolveComponentGroups
)
_ = registerExtendedComponentGroup(
pkg.libraryName,
componentGroup
)
} yield ()
}
}
}
/** Register the list of component groups defined by a library.
*
* @param library the library name
* @param newGroups the list of component groups that the library defines
*/
private def registerComponentGroups(
library: LibraryName,
newGroups: List[ComponentGroup]
): Unit =
loadedComponents.updateWith(library) {
case Some(groups) =>
Some(groups.copy(newGroups = groups.newGroups ::: newGroups))
case None =>
Some(ComponentGroups(newGroups, List()))
}
/** Register a component group extended by a library.
*
* @param library the library name
* @param group the extended component group
*/
private def registerExtendedComponentGroup(
library: LibraryName,
group: ExtendedComponentGroup
): Unit =
loadedComponents.updateWith(library) {
case Some(groups) =>
Some(groups.copy(extendedGroups = groups.extendedGroups :+ group))
case None =>
Some(ComponentGroups(List(), List(group)))
}
/** @inheritdoc */
override def ensurePackageIsLoaded(
libraryName: LibraryName
): Either[Error, Unit] =
if (loadedPackages.contains(libraryName)) Right(())
else {
logger.trace(s"Resolving library $libraryName.")
val resolvedLibrary =
libraryProvider
.findLibrary(libraryName)
.left
.map {
case ResolvingLibraryProvider.Error.NotResolved(details) =>
Error.PackageCouldNotBeResolved(details)
case ResolvingLibraryProvider.Error.DownloadFailed(_, reason) =>
Error.PackageDownloadFailed(reason)
case ResolvingLibraryProvider.Error.RequestedLocalLibraryDoesNotExist =>
Error.PackageLoadingError(
"The local library has not been found on the local " +
"libraries search paths."
)
}
resolvedLibrary match {
case Left(error) =>
logger.warn(s"Resolution failed with [$error].", error)
case Right(resolved) =>
logger.info(
s"Found library ${resolved.name} @ ${resolved.version} " +
s"at [${MaskedPath(resolved.root.location).applyMasking()}]."
)
}
this.synchronized {
// We check again inside of the monitor, in case that some other
// thread has just added this library.
if (loadedPackages.contains(libraryName)) Right(())
else
resolvedLibrary
.flatMap { library =>
loadPackage(library.name, library.version, library.root)
}
.flatMap(resolveComponentGroups)
}
}
/** @inheritdoc */
def isPackageLoaded(libraryName: LibraryName): Boolean = {
loadedPackages.keySet.contains(libraryName)
}
/** @inheritdoc */
override def getLoadedModules: Seq[Module] =
loadedModules.values.toSeq
/** @inheritdoc */
override def getLoadedPackages: Seq[Package[TruffleFile]] =
loadedPackages.values.toSeq.flatten
override def getLoadedPackagesJava
: java.lang.Iterable[Package[TruffleFile]] =
loadedPackages.flatMap(_._2).asJava
/** @inheritdoc */
override def getLoadedModule(qualifiedName: String): Option[Module] =
loadedModules.get(qualifiedName)
/** @inheritdoc */
override def registerModuleCreatedInRuntime(module: Module): Unit =
registerModule(module)
private def registerModule(module: Module): Unit = {
loadedModules.put(module.getName.toString, module)
}
override def registerSyntheticPackage(
namespace: String,
name: String
): Unit =
loadedPackages.put(LibraryName(namespace, name), None)
/** Registering synthetic module, unlike the non-compiler generated one, is conditional
* in a sense that if a module already exists with a given name we only update its
* list of synthetic modules that it should export.
* If no module exists under the given name, we register the synthetic one.
*
* @param syntheticModule a synthetic module to register
* @param refs list of names of modules that should be exported by the module under the given name
*/
private def registerSyntheticModule(
syntheticModule: Module,
refs: List[QualifiedName]
): Unit = {
assert(syntheticModule.isSynthetic)
if (!loadedModules.contains(syntheticModule.getName.toString)) {
loadedModules.put(syntheticModule.getName.toString, syntheticModule)
} else {
val loaded = loadedModules(syntheticModule.getName.toString)
assert(!loaded.isSynthetic)
loaded.setDirectModulesRefs(refs.asJava)
}
}
override def deregisterModule(qualifiedName: String): Unit =
loadedModules.remove(qualifiedName)
/** @inheritdoc */
override def renameProject(
namespace: String,
oldName: String,
newName: String
): Unit = this.synchronized {
renamePackages(namespace, oldName, newName)
renameModules(namespace, oldName, newName)
}
private def renamePackages(
namespace: String,
oldName: String,
newName: String
): Unit = {
val toChange = loadedPackages.toSeq.filter { case (name, _) =>
name.namespace == namespace && name.name == oldName
}
for ((key, _) <- toChange) {
loadedPackages.remove(key)
}
for ((key, pkgOption) <- toChange) {
val newPkg = pkgOption.map(_.setPackageName(newName))
val newKey = key.copy(name = newName)
loadedPackages.put(newKey, newPkg)
}
}
private def renameModules(
namespace: String,
oldName: String,
newName: String
): Unit = {
val separator: String = QualifiedName.separator
val keys = loadedModules.keySet.filter(name =>
name.startsWith(namespace + separator + oldName + separator)
)
for {
key <- keys
module <- loadedModules.remove(key)
} {
module.renameProject(newName)
loadedModules.put(module.getName.toString, module)
}
}
override def isNamespaceRegistered(namespace: String): Boolean =
loadedPackages.keySet.exists(_.namespace == namespace)
override def getPackageForLibrary(
libraryName: LibraryName
): Option[Package[TruffleFile]] =
loadedPackages.get(libraryName).flatten
override def getModulesForLibrary(libraryName: LibraryName): List[Module] =
getPackageForLibrary(libraryName)
.map(pkg => loadedModules.values.filter(_.getPackage == pkg).toList)
.getOrElse(Nil)
override def getLibraryBindings(
libraryName: LibraryName,
serializationManager: SerializationManager
): Option[ImportExportCache.CachedBindings] = {
ensurePackageIsLoaded(libraryName).toOption.flatMap { _ =>
if (!loadedLibraryBindings.contains(libraryName)) {
loadedPackages.get(libraryName).flatten.foreach(loadDependencies(_))
serializationManager
.deserializeLibraryBindings(libraryName)
.foreach(cache =>
loadedLibraryBindings.addOne((libraryName, cache))
)
}
loadedLibraryBindings.get(libraryName)
}
}
private def loadDependencies(pkg: Package[TruffleFile]): Unit = {
val manifestFile = fs.getChild(pkg.root, LibraryManifest.filename)
readManifest(manifestFile)
.flatMap(LibraryManifest.fromYaml(_))
.foreach(
_.dependencies.foreach(ensurePackageIsLoaded)
)
}
private def readManifest(file: TruffleFile): Try[String] = {
if (file.exists())
Using(file.newBufferedReader) { reader =>
StringUtils.join(reader.lines().iterator(), "\n")
}
else Failure(PackageManager.PackageNotFound())
}
}
/** Creates a [[PackageRepository]] for the run.
*
* It tries to load and resolve the edition used in the project (or the
* default edition), so that any libraries to be loaded can be resolved using
* that edition.
*
* Edition and library search paths are based on the distribution and
* language home (if it is provided).
*
* @param projectPackage the package of the current project (if ran inside of a project)
* @param languageHome the language home (if set)
* @param distributionManager the distribution manager
* @param resourceManager the resource manager instance
* @param context the context reference, needed to add polyglot libraries to
* the classpath
* @param builtins the builtins that are always preloaded
* @param notificationHandler a handler for library addition and progress
* notifications
* @return an initialized [[PackageRepository]]
*/
def initializeRepository(
projectPackage: Option[Package[TruffleFile]],
languageHome: Option[String],
editionOverride: Option[String],
distributionManager: DistributionManager,
resourceManager: ResourceManager,
context: EnsoContext,
builtins: Builtins,
notificationHandler: NotificationHandler
): PackageRepository = {
val rawEdition = editionOverride
.map(v => Editions.Raw.Edition(parent = Some(v)))
.orElse(
projectPackage
.flatMap(_.getConfig().edition)
)
.getOrElse(DefaultEdition.getDefaultEdition)
val homeManager = languageHome.map { home => LanguageHome(Path.of(home)) }
val editionManager = EditionManager(distributionManager, homeManager)
val edition = editionManager.resolveEdition(rawEdition).get
val resolvingLibraryProvider =
DefaultLibraryProvider.make(
distributionManager = distributionManager,
resourceManager = resourceManager,
lockUserInterface = notificationHandler,
progressReporter = notificationHandler,
languageHome = homeManager,
edition = edition,
preferLocalLibraries =
projectPackage.exists(_.getConfig().preferLocalLibraries)
)
new Default(
resolvingLibraryProvider,
context,
builtins,
notificationHandler
)
}
} }

View File

@ -0,0 +1,650 @@
package org.enso.interpreter.runtime
import org.enso.compiler.PackageRepository
import org.enso.compiler.ImportExportCache
import org.enso.compiler.SerializationManager
import com.oracle.truffle.api.TruffleFile
import com.typesafe.scalalogging.Logger
import org.apache.commons.lang3.StringUtils
import org.enso.editions.LibraryVersion
import org.enso.interpreter.runtime.util.TruffleFileSystem
import org.enso.librarymanager.published.repository.LibraryManifest
import org.enso.librarymanager.resolved.LibraryRoot
import org.enso.librarymanager.{ResolvingLibraryProvider}
import org.enso.logger.masking.MaskedPath
import org.enso.pkg.{
Component,
ComponentGroup,
ExtendedComponentGroup,
PackageManager,
QualifiedName,
SourceFile
}
import org.enso.text.buffer.Rope
import org.enso.polyglot.CompilationStage
import java.nio.file.Path
import scala.collection.immutable.ListSet
import scala.jdk.CollectionConverters.{IterableHasAsJava, SeqHasAsJava}
import scala.util.{Failure, Try, Using}
import org.enso.distribution.locking.ResourceManager
import org.enso.distribution.{DistributionManager, LanguageHome}
import org.enso.editions.updater.EditionManager
import org.enso.editions.{DefaultEdition, Editions, LibraryName}
import org.enso.interpreter.instrument.NotificationHandler
import org.enso.interpreter.runtime.builtin.Builtins
import org.enso.interpreter.runtime.{EnsoContext, Module}
import org.enso.librarymanager.{DefaultLibraryProvider}
import org.enso.pkg.{ComponentGroups, Package}
/** The default [[PackageRepository]] implementation.
*
* @param libraryProvider the [[ResolvingLibraryProvider]] which resolves
* which library version should be imported and
* locates them (or downloads if they are missing)
* @param context the language context
* @param builtins the builtins module
* @param notificationHandler a notification handler
*/
private class DefaultPackageRepository(
libraryProvider: ResolvingLibraryProvider,
context: EnsoContext,
builtins: Builtins,
notificationHandler: NotificationHandler
) extends PackageRepository {
private val logger = Logger[DefaultPackageRepository]
implicit private val fs: TruffleFileSystem = new TruffleFileSystem
private val packageManager = new PackageManager[TruffleFile]
private var projectPackage: Option[Package[TruffleFile]] = None
/** The mapping containing all loaded packages.
*
* It should be modified only from within synchronized sections, but it may
* be always read. Thus elements should be added to this mapping only after
* all library loading bookkeeping has been finished - so that if other,
* unsynchronized threads read this map, every element it contains is
* already fully processed.
*/
private val loadedPackages
: collection.mutable.Map[LibraryName, Option[Package[TruffleFile]]] = {
val builtinsName = LibraryName(Builtins.NAMESPACE, Builtins.PACKAGE_NAME)
collection.mutable.LinkedHashMap(builtinsName -> None)
}
/** The mapping containing loaded modules.
*
* We use [[String]] as the key as we often index into this map based on
* qualified names that come from interop (via
* [[org.enso.interpreter.runtime.scope.TopLevelScope]]). These arrive as
* Strings, and constantly converting them into [[QualifiedName]]s would
* add more overhead than is probably necessary.
*/
private val loadedModules: collection.concurrent.Map[String, Module] =
collection.concurrent.TrieMap(Builtins.MODULE_NAME -> builtins.getModule)
/** The mapping containing loaded component groups.
*
* It should be modified and read only from within synchronized sections.
* The component mapping is added to the collection after ensuring that the
* corresponding library was loaded.
*/
private val loadedComponents
: collection.mutable.Map[LibraryName, ComponentGroups] = {
val builtinsName = LibraryName(Builtins.NAMESPACE, Builtins.PACKAGE_NAME)
collection.mutable.LinkedHashMap(builtinsName -> ComponentGroups.empty)
}
/** The mapping between the library and its cached bindings, if already laoded. */
private val loadedLibraryBindings: collection.mutable.Map[
LibraryName,
ImportExportCache.CachedBindings
] =
collection.mutable.LinkedHashMap()
private def getComponentModules: ListSet[Module] = {
val modules = for {
componentGroups <- loadedComponents.values
newComponents = componentGroups.newGroups.flatMap(_.exports)
extendedComponents = componentGroups.extendedGroups.flatMap(_.exports)
component <- newComponents ++ extendedComponents
module <- findComponentModule(component)
} yield module
modules.to(ListSet)
}
private def findComponentModule(component: Component): Option[Module] = {
def mkModuleName(path: Array[String]): String =
path.mkString(LibraryName.separator.toString)
@scala.annotation.tailrec
def go(path: Array[String]): Option[Module] =
if (path.isEmpty) None
else {
loadedModules.get(mkModuleName(path)) match {
case Some(module) => Some(module)
case None => go(path.init)
}
}
go(component.name.split(LibraryName.separator))
}
/** @inheritdoc */
override def getModuleMap: PackageRepository.ModuleMap = loadedModules
/** @inheritdoc */
override def freezeModuleMap: PackageRepository.FrozenModuleMap =
loadedModules.toMap
/** @inheritdoc */
override def getComponents: PackageRepository.ComponentsMap =
this.synchronized {
loadedComponents.toMap
}
/** @inheritdoc */
override def getPendingModules: ListSet[Module] =
this.synchronized {
for {
module <- getComponentModules
isCompiled =
module.getCompilationStage.isAtLeast(
CompilationStage.AFTER_CODEGEN
)
if !isCompiled
} yield module
}
/** @inheritdoc */
override def registerMainProjectPackage(
libraryName: LibraryName,
pkg: Package[TruffleFile]
): Unit = {
projectPackage = Some(pkg)
registerPackageInternal(
libraryName = libraryName,
pkg = pkg,
libraryVersion = LibraryVersion.Local,
isLibrary = false
)
}
/** @inheritdoc */
override def getMainProjectPackage: Option[Package[TruffleFile]] = {
projectPackage
}
private def registerPackageInternal(
libraryName: LibraryName,
libraryVersion: LibraryVersion,
pkg: Package[TruffleFile],
isLibrary: Boolean
): Unit = {
val extensions = pkg.listPolyglotExtensions("java")
extensions.foreach(context.getEnvironment.addToHostClassPath)
val (regularModules, syntheticModulesMetadata) = pkg
.listSources()
.map(srcFile =>
(
new Module(srcFile.qualifiedName, pkg, srcFile.file),
inferSyntheticModules(srcFile)
)
)
.unzip
regularModules.foreach(registerModule)
syntheticModulesMetadata.flatten
.groupMap(_._1)(v => (v._2, v._3))
.foreach { case (qName, modulesWithSources) =>
val source = modulesWithSources
.map(_._2)
.foldLeft("")(_ ++ "\n" ++ _)
registerSyntheticModule(
Module.synthetic(
qName,
pkg,
Rope(source)
),
modulesWithSources.map(_._1)
)
}
if (isLibrary) {
val root = Path.of(pkg.root.toString)
notificationHandler.addedLibrary(libraryName, libraryVersion, root)
}
loadedPackages.put(libraryName, Some(pkg))
}
/** For any given source file, infer data necessary to generate synthetic modules as well as their contents.
* E.g., for A/B/C.enso it infers modules
* - A.B that exports A.B.C
* - A that exports A.B
*
* @param srcFile Enso source file to consider
* @return a list of triples representing the name of submodule along the path, what submodule it exports and its contents
*/
private def inferSyntheticModules(
srcFile: SourceFile[TruffleFile]
): List[(QualifiedName, QualifiedName, String)] = {
def listAllIntermediateModules(
namespace: String,
name: String,
elements: List[String],
exportItem: String
): List[(QualifiedName, QualifiedName, String)] = {
elements match {
case Nil =>
Nil
case lastModuleName :: parts =>
val pathElems = elements.reverse
val modName =
s"${namespace}.$name.${pathElems.mkString(".")}.$exportItem"
val modSource =
s"""|import $modName
|export $modName
|""".stripMargin
val syntheticModuleInfo = (
QualifiedName(namespace :: name :: parts.reverse, lastModuleName),
QualifiedName(namespace :: name :: pathElems, exportItem),
modSource
)
syntheticModuleInfo :: listAllIntermediateModules(
namespace,
name,
parts,
lastModuleName
)
}
}
srcFile.qualifiedName.path match {
case namespace :: name :: rest =>
listAllIntermediateModules(
namespace,
name,
rest.reverse,
srcFile.qualifiedName.item
)
case _ =>
Nil
}
}
/** This package modifies the [[loadedPackages]], so it should be only
* called from within synchronized sections.
*/
private def loadPackage(
libraryName: LibraryName,
libraryVersion: LibraryVersion,
root: LibraryRoot
): Either[PackageRepository.Error, Package[TruffleFile]] = Try {
logger.debug(
s"Loading library $libraryName from " +
s"[${MaskedPath(root.location).applyMasking()}]."
)
val rootFile = context.getEnvironment.getInternalTruffleFile(
root.location.toAbsolutePath.normalize.toString
)
val pkg = packageManager.loadPackage(rootFile).get
registerPackageInternal(
libraryName = libraryName,
libraryVersion = libraryVersion,
pkg = pkg,
isLibrary = true
)
pkg
}.toEither.left.map { error =>
PackageRepository.Error.PackageLoadingError(error.getMessage)
}
/** @inheritdoc */
override def initialize(): Either[PackageRepository.Error, Unit] =
this.synchronized {
val unprocessedPackages =
loadedPackages.keySet
.diff(loadedComponents.keySet)
.flatMap(loadedPackages(_))
unprocessedPackages.foldLeft[Either[PackageRepository.Error, Unit]](
Right(())
) { (accumulator, pkg) =>
for {
_ <- accumulator
_ <- resolveComponentGroups(pkg)
} yield ()
}
}
private def resolveComponentGroups(
pkg: Package[TruffleFile]
): Either[PackageRepository.Error, Unit] =
if (loadedComponents.contains(pkg.libraryName)) Right(())
else {
pkg.getConfig().componentGroups match {
case Left(err) =>
Left(PackageRepository.Error.PackageLoadingError(err.getMessage()))
case Right(componentGroups) =>
logger.debug(
s"Resolving component groups of package [${pkg.name}]."
)
registerComponentGroups(pkg.libraryName, componentGroups.newGroups)
componentGroups.extendedGroups
.foldLeft[Either[PackageRepository.Error, Unit]](Right(())) {
(accumulator, componentGroup) =>
for {
_ <- accumulator
extendedLibraryName = componentGroup.group.libraryName
_ <- ensurePackageIsLoaded(extendedLibraryName)
pkgOpt = loadedPackages(extendedLibraryName)
_ <- pkgOpt.fold[Either[PackageRepository.Error, Unit]](
Right(())
)(
resolveComponentGroups
)
_ = registerExtendedComponentGroup(
pkg.libraryName,
componentGroup
)
} yield ()
}
}
}
/** Register the list of component groups defined by a library.
*
* @param library the library name
* @param newGroups the list of component groups that the library defines
*/
private def registerComponentGroups(
library: LibraryName,
newGroups: List[ComponentGroup]
): Unit =
loadedComponents.updateWith(library) {
case Some(groups) =>
Some(groups.copy(newGroups = groups.newGroups ::: newGroups))
case None =>
Some(ComponentGroups(newGroups, List()))
}
/** Register a component group extended by a library.
*
* @param library the library name
* @param group the extended component group
*/
private def registerExtendedComponentGroup(
library: LibraryName,
group: ExtendedComponentGroup
): Unit =
loadedComponents.updateWith(library) {
case Some(groups) =>
Some(groups.copy(extendedGroups = groups.extendedGroups :+ group))
case None =>
Some(ComponentGroups(List(), List(group)))
}
/** @inheritdoc */
override def ensurePackageIsLoaded(
libraryName: LibraryName
): Either[PackageRepository.Error, Unit] =
if (loadedPackages.contains(libraryName)) Right(())
else {
logger.trace(s"Resolving library $libraryName.")
val resolvedLibrary =
libraryProvider
.findLibrary(libraryName)
.left
.map {
case ResolvingLibraryProvider.Error.NotResolved(details) =>
PackageRepository.Error.PackageCouldNotBeResolved(details)
case ResolvingLibraryProvider.Error.DownloadFailed(_, reason) =>
PackageRepository.Error.PackageDownloadFailed(reason)
case ResolvingLibraryProvider.Error.RequestedLocalLibraryDoesNotExist =>
PackageRepository.Error.PackageLoadingError(
"The local library has not been found on the local " +
"libraries search paths."
)
}
resolvedLibrary match {
case Left(error) =>
logger.warn(s"Resolution failed with [$error].", error)
case Right(resolved) =>
logger.info(
s"Found library ${resolved.name} @ ${resolved.version} " +
s"at [${MaskedPath(resolved.root.location).applyMasking()}]."
)
}
this.synchronized {
// We check again inside of the monitor, in case that some other
// thread has just added this library.
if (loadedPackages.contains(libraryName)) Right(())
else
resolvedLibrary
.flatMap { library =>
loadPackage(library.name, library.version, library.root)
}
.flatMap(resolveComponentGroups)
}
}
/** @inheritdoc */
def isPackageLoaded(libraryName: LibraryName): Boolean = {
loadedPackages.keySet.contains(libraryName)
}
/** @inheritdoc */
override def getLoadedModules: Seq[Module] =
loadedModules.values.toSeq
/** @inheritdoc */
override def getLoadedPackages: Seq[Package[TruffleFile]] =
loadedPackages.values.toSeq.flatten
override def getLoadedPackagesJava: java.lang.Iterable[Package[TruffleFile]] =
loadedPackages.flatMap(_._2).asJava
/** @inheritdoc */
override def getLoadedModule(qualifiedName: String): Option[Module] =
loadedModules.get(qualifiedName)
/** @inheritdoc */
override def registerModuleCreatedInRuntime(module: Module): Unit =
registerModule(module)
private def registerModule(module: Module): Unit = {
loadedModules.put(module.getName.toString, module)
}
override def registerSyntheticPackage(
namespace: String,
name: String
): Unit =
loadedPackages.put(LibraryName(namespace, name), None)
/** Registering synthetic module, unlike the non-compiler generated one, is conditional
* in a sense that if a module already exists with a given name we only update its
* list of synthetic modules that it should export.
* If no module exists under the given name, we register the synthetic one.
*
* @param syntheticModule a synthetic module to register
* @param refs list of names of modules that should be exported by the module under the given name
*/
private def registerSyntheticModule(
syntheticModule: Module,
refs: List[QualifiedName]
): Unit = {
assert(syntheticModule.isSynthetic)
if (!loadedModules.contains(syntheticModule.getName.toString)) {
loadedModules.put(syntheticModule.getName.toString, syntheticModule)
} else {
val loaded = loadedModules(syntheticModule.getName.toString)
assert(!loaded.isSynthetic)
loaded.setDirectModulesRefs(refs.asJava)
}
}
override def deregisterModule(qualifiedName: String): Unit =
loadedModules.remove(qualifiedName)
/** @inheritdoc */
override def renameProject(
namespace: String,
oldName: String,
newName: String
): Unit = this.synchronized {
renamePackages(namespace, oldName, newName)
renameModules(namespace, oldName, newName)
}
private def renamePackages(
namespace: String,
oldName: String,
newName: String
): Unit = {
val toChange = loadedPackages.toSeq.filter { case (name, _) =>
name.namespace == namespace && name.name == oldName
}
for ((key, _) <- toChange) {
loadedPackages.remove(key)
}
for ((key, pkgOption) <- toChange) {
val newPkg = pkgOption.map(_.setPackageName(newName))
val newKey = key.copy(name = newName)
loadedPackages.put(newKey, newPkg)
}
}
private def renameModules(
namespace: String,
oldName: String,
newName: String
): Unit = {
val separator: String = QualifiedName.separator
val keys = loadedModules.keySet.filter(name =>
name.startsWith(namespace + separator + oldName + separator)
)
for {
key <- keys
module <- loadedModules.remove(key)
} {
module.renameProject(newName)
loadedModules.put(module.getName.toString, module)
}
}
override def isNamespaceRegistered(namespace: String): Boolean =
loadedPackages.keySet.exists(_.namespace == namespace)
override def getPackageForLibrary(
libraryName: LibraryName
): Option[Package[TruffleFile]] =
loadedPackages.get(libraryName).flatten
override def getModulesForLibrary(libraryName: LibraryName): List[Module] =
getPackageForLibrary(libraryName)
.map(pkg => loadedModules.values.filter(_.getPackage == pkg).toList)
.getOrElse(Nil)
override def getLibraryBindings(
libraryName: LibraryName,
serializationManager: SerializationManager
): Option[ImportExportCache.CachedBindings] = {
ensurePackageIsLoaded(libraryName).toOption.flatMap { _ =>
if (!loadedLibraryBindings.contains(libraryName)) {
loadedPackages.get(libraryName).flatten.foreach(loadDependencies(_))
serializationManager
.deserializeLibraryBindings(libraryName)
.foreach(cache => loadedLibraryBindings.addOne((libraryName, cache)))
}
loadedLibraryBindings.get(libraryName)
}
}
private def loadDependencies(pkg: Package[TruffleFile]): Unit = {
val manifestFile = fs.getChild(pkg.root, LibraryManifest.filename)
readManifest(manifestFile)
.flatMap(LibraryManifest.fromYaml(_))
.foreach(
_.dependencies.foreach(ensurePackageIsLoaded)
)
}
private def readManifest(file: TruffleFile): Try[String] = {
if (file.exists())
Using(file.newBufferedReader) { reader =>
StringUtils.join(reader.lines().iterator(), "\n")
}
else Failure(PackageManager.PackageNotFound())
}
}
private object DefaultPackageRepository {
/** Creates a [[PackageRepository]] for the run.
*
* It tries to load and resolve the edition used in the project (or the
* default edition), so that any libraries to be loaded can be resolved using
* that edition.
*
* Edition and library search paths are based on the distribution and
* language home (if it is provided).
*
* @param projectPackage the package of the current project (if ran inside of a project)
* @param languageHome the language home (if set)
* @param distributionManager the distribution manager
* @param resourceManager the resource manager instance
* @param context the context reference, needed to add polyglot libraries to
* the classpath
* @param builtins the builtins that are always preloaded
* @param notificationHandler a handler for library addition and progress
* notifications
* @return an initialized [[PackageRepository]]
*/
def initializeRepository(
projectPackage: Option[Package[TruffleFile]],
languageHome: Option[String],
editionOverride: Option[String],
distributionManager: DistributionManager,
resourceManager: ResourceManager,
context: EnsoContext,
builtins: Builtins,
notificationHandler: NotificationHandler
): PackageRepository = {
val rawEdition = editionOverride
.map(v => Editions.Raw.Edition(parent = Some(v)))
.orElse(
projectPackage
.flatMap(_.getConfig().edition)
)
.getOrElse(DefaultEdition.getDefaultEdition)
val homeManager = languageHome.map { home => LanguageHome(Path.of(home)) }
val editionManager = EditionManager(distributionManager, homeManager)
val edition = editionManager.resolveEdition(rawEdition).get
val resolvingLibraryProvider =
DefaultLibraryProvider.make(
distributionManager = distributionManager,
resourceManager = resourceManager,
lockUserInterface = notificationHandler,
progressReporter = notificationHandler,
languageHome = homeManager,
edition = edition,
preferLocalLibraries =
projectPackage.exists(_.getConfig().preferLocalLibraries)
)
new DefaultPackageRepository(
resolvingLibraryProvider,
context,
builtins,
notificationHandler
)
}
}