Define Editions (#1797)

This commit is contained in:
Radosław Waśko 2021-06-18 16:39:45 +02:00 committed by GitHub
parent c819f42130
commit 241a1e7d74
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
138 changed files with 2669 additions and 578 deletions

View File

@ -65,6 +65,13 @@ jobs:
uses: actions/checkout@v2
with:
path: repo
- name: Configure Pagefile (Windows)
if: runner.os == 'Windows'
uses: al-cheb/configure-pagefile-action@v1.2
with:
minimum-size: 16GB
maximum-size: 16GB
disk-root: "C:"
- name: Enable Developer Command Prompt (Windows)
uses: ilammy/msvc-dev-cmd@v1.9.0
- name: Disable TCP/UDP Offloading (macOS)

View File

@ -32,6 +32,13 @@ jobs:
uses: actions/checkout@v2
with:
path: repo
- name: Configure Pagefile (Windows)
if: runner.os == 'Windows'
uses: al-cheb/configure-pagefile-action@v1.2
with:
minimum-size: 16GB
maximum-size: 16GB
disk-root: "C:"
- name: Enable Developer Command Prompt (Windows)
uses: ilammy/msvc-dev-cmd@v1.9.0
- name: Disable TCP/UDP Offloading (macOS)

View File

@ -27,6 +27,13 @@ jobs:
fail-fast: false
steps:
- uses: actions/checkout@v2
- name: Configure Pagefile (Windows)
if: runner.os == 'Windows'
uses: al-cheb/configure-pagefile-action@v1.2
with:
minimum-size: 16GB
maximum-size: 16GB
disk-root: "C:"
- name: Enable Developer Command Prompt (Windows)
uses: ilammy/msvc-dev-cmd@v1.9.0
- name: Setup Go

View File

@ -39,6 +39,18 @@
- Added support for multiple content roots in the language server
([#1800](https://github.com/enso-org/enso/pull/1800/)). It is not yet exposed
to the IDE, as this will be done as part of future work.
- Modified the `package.yaml` format in preparation for the library ecosystem
([#1797](https://github.com/enso-org/enso/pull/1797)). The `engine-version`
field has been deprecated in favour of an `edition` field that allows to set
up the engine version and dependency resolution using the upcoming Edition
system. New tools will still be able to read the old format, but upon
modification, they will save changes in the new format. As the `edition` file
did not exist in the older version, old tools will actually correctly load the
migrated package file (as we allow for unknown fields), but they will not know
how to interpret the new `edition` field and so will fall back to using the
`default` engine version, which may be unexpected. Ideally, after migration,
the project should be used only with the new tools. The affected tools are the
Launcher and the Project Manager.
## Libraries

View File

@ -583,11 +583,11 @@ lazy val pkg = (project in file("lib/scala/pkg"))
version := "0.1",
libraryDependencies ++= circe ++ Seq(
"org.scalatest" %% "scalatest" % scalatestVersion % Test,
"nl.gn0s1s" %% "bump" % bumpVersion,
"io.circe" %% "circe-yaml" % circeYamlVersion, // separate from other circe deps because its independent project with its own versioning
"commons-io" % "commons-io" % commonsIoVersion
)
)
.dependsOn(editions)
lazy val `akka-native` = project
.in(file("lib/scala/akka-native"))
@ -728,9 +728,12 @@ lazy val `project-manager` = (project in file("lib/scala/project-manager"))
)
.dependsOn(`akka-native`)
.dependsOn(`version-output`)
.dependsOn(pkg)
.dependsOn(editions)
.dependsOn(cli)
.dependsOn(`polyglot-api`)
.dependsOn(`runtime-version-manager`)
.dependsOn(`library-manager`)
.dependsOn(pkg)
.dependsOn(`json-rpc-server`)
.dependsOn(`json-rpc-server-test` % Test)
.dependsOn(testkit % Test)
@ -883,6 +886,7 @@ lazy val `polyglot-api` = project
.dependsOn(pkg)
.dependsOn(`text-buffer`)
.dependsOn(`logging-utils`)
.dependsOn(testkit % Test)
lazy val `language-server` = (project in file("engine/language-server"))
.settings(
@ -1184,8 +1188,53 @@ lazy val launcher = project
.dependsOn(`version-output`)
.dependsOn(pkg)
.dependsOn(`logging-service`)
.dependsOn(`distribution-manager` % Test)
.dependsOn(`runtime-version-manager-test` % Test)
lazy val `distribution-manager` = project
.in(file("lib/scala/distribution-manager"))
.configs(Test)
.settings(
resolvers += Resolver.bintrayRepo("gn0s1s", "releases"),
libraryDependencies ++= Seq(
"com.typesafe.scala-logging" %% "scala-logging" % scalaLoggingVersion,
"io.circe" %% "circe-yaml" % circeYamlVersion,
"commons-io" % "commons-io" % commonsIoVersion,
"org.scalatest" %% "scalatest" % scalatestVersion % Test
)
)
.dependsOn(editions)
.dependsOn(pkg)
.dependsOn(`logging-utils`)
lazy val editions = project
.in(file("lib/scala/editions"))
.configs(Test)
.settings(
resolvers += Resolver.bintrayRepo("gn0s1s", "releases"),
libraryDependencies ++= Seq(
"com.typesafe.scala-logging" %% "scala-logging" % scalaLoggingVersion,
"nl.gn0s1s" %% "bump" % bumpVersion,
"io.circe" %% "circe-yaml" % circeYamlVersion,
"org.scalatest" %% "scalatest" % scalatestVersion % Test
)
)
lazy val `library-manager` = project
.in(file("lib/scala/library-manager"))
.configs(Test)
.settings(
resolvers += Resolver.bintrayRepo("gn0s1s", "releases"),
libraryDependencies ++= Seq(
"com.typesafe.scala-logging" %% "scala-logging" % scalaLoggingVersion,
"org.scalatest" %% "scalatest" % scalatestVersion % Test
)
)
.dependsOn(editions)
.dependsOn(cli)
.dependsOn(`distribution-manager`)
.dependsOn(testkit % Test)
lazy val `runtime-version-manager` = project
.in(file("lib/scala/runtime-version-manager"))
.configs(Test)
@ -1204,6 +1253,7 @@ lazy val `runtime-version-manager` = project
.dependsOn(`logging-service`)
.dependsOn(cli)
.dependsOn(`version-output`)
.dependsOn(`distribution-manager`)
lazy val `runtime-version-manager-test` = project
.in(file("lib/scala/runtime-version-manager-test"))
@ -1224,6 +1274,8 @@ lazy val `runtime-version-manager-test` = project
.dependsOn(`runtime-version-manager`)
.dependsOn(`logging-service`)
.dependsOn(testkit)
.dependsOn(cli)
.dependsOn(`distribution-manager`)
lazy val `locking-test-helper` = project
.in(file("lib/scala/locking-test-helper"))

View File

@ -26,6 +26,11 @@ The license information can be found along with the copyright notices.
Copyright notices related to this dependency can be found in the directory `com.thoughtworks.paranamer.paranamer-2.8`.
'slf4j-api', licensed under the MIT License, is distributed with the engine.
The license file can be found at `licenses/MIT`.
Copyright notices related to this dependency can be found in the directory `org.slf4j.slf4j-api-1.7.26`.
'izumi-reflect_2.13', licensed under the BSD-style, is distributed with the engine.
The license information can be found along with the copyright notices.
Copyright notices related to this dependency can be found in the directory `dev.zio.izumi-reflect_2.13-1.0.0-M5`.
@ -246,11 +251,6 @@ The license file can be found at `licenses/APACHE2.0`.
Copyright notices related to this dependency can be found in the directory `com.typesafe.scala-logging.scala-logging_2.13-3.9.2`.
'slf4j-api', licensed under the MIT License, is distributed with the engine.
The license file can be found at `licenses/MIT`.
Copyright notices related to this dependency can be found in the directory `org.slf4j.slf4j-api-1.7.25`.
'commons-cli', licensed under the Apache License, Version 2.0, is distributed with the engine.
The license file can be found at `licenses/APACHE2.0`.
Copyright notices related to this dependency can be found in the directory `commons-cli.commons-cli-1.4`.

View File

@ -1,19 +1,3 @@
/*
* Copyright 2008 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/*
* Copyright 2013 Google LLC
*
@ -43,3 +27,19 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/*
* Copyright 2008 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

View File

@ -1,5 +1,5 @@
/*
* Copyright 2014 The Error Prone Authors.
* Copyright 2016 The Error Prone Authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -15,7 +15,7 @@
*/
/*
* Copyright 2016 The Error Prone Authors.
* Copyright 2015 The Error Prone Authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -47,7 +47,7 @@
*/
/*
* Copyright 2015 The Error Prone Authors.
* Copyright 2014 The Error Prone Authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.

View File

@ -1,7 +1,7 @@
/*
* Copyright (C) 2009-2020 Lightbend Inc. <https://www.lightbend.com>
*/
/*
* Copyright (C) 2017-2020 Lightbend Inc. <https://www.lightbend.com>
*/
/*
* Copyright (C) 2009-2020 Lightbend Inc. <https://www.lightbend.com>
*/

View File

@ -15,8 +15,6 @@
* limitations under the License.
*/
Copyright 2017-2019 John A. De Goes and the ZIO Contributors
/*
* Copyright 2017-2020 John A. De Goes and the ZIO Contributors
* Copyright 2017-2018 Łukasz Biały, Paul Chiusano, Michael Pilquist,
@ -36,3 +34,5 @@ Copyright 2017-2019 John A. De Goes and the ZIO Contributors
* See the License for the specific language governing permissions and
* limitations under the License.
*/
Copyright 2017-2019 John A. De Goes and the ZIO Contributors

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2011,2012 Mathias Doenitz, Johannes Rudolph
* Copyright (C) 2011 Mathias Doenitz
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -15,7 +15,7 @@
*/
/*
* Copyright (C) 2011 Mathias Doenitz
* Copyright (C) 2011,2012 Mathias Doenitz, Johannes Rudolph
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.

View File

@ -1,7 +1,7 @@
/*
* Copyright (C) 2009-2020 Lightbend Inc. <https://www.lightbend.com>
*/
/*
* Copyright (C) 2017-2020 Lightbend Inc. <https://www.lightbend.com>
*/
/*
* Copyright (C) 2009-2020 Lightbend Inc. <https://www.lightbend.com>
*/

View File

@ -15,8 +15,6 @@
* limitations under the License.
*/
Copyright 2017-2019 John A. De Goes and the ZIO Contributors
/*
* Copyright 2017-2020 John A. De Goes and the ZIO Contributors
* Copyright 2017-2018 Łukasz Biały, Paul Chiusano, Michael Pilquist,
@ -36,3 +34,5 @@ Copyright 2017-2019 John A. De Goes and the ZIO Contributors
* See the License for the specific language governing permissions and
* limitations under the License.
*/
Copyright 2017-2019 John A. De Goes and the ZIO Contributors

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2011,2012 Mathias Doenitz, Johannes Rudolph
* Copyright (C) 2011 Mathias Doenitz
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -15,7 +15,7 @@
*/
/*
* Copyright (C) 2011 Mathias Doenitz
* Copyright (C) 2011,2012 Mathias Doenitz, Johannes Rudolph
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.

View File

@ -47,3 +47,4 @@ It is broken up into categories as follows:
- [**Parser:**](./parser) Design and specification of the Enso parser.
- [**Infrastructure:**](./infrastructure) Description of the infrastructure for
building Enso.
- [**Libraries:**](./libraries) Description of Enso's Library Ecosystem.

View File

@ -20,7 +20,7 @@ structured and how it should behave.
- [Installing from a Portable Distribution](#installing-from-a-portable-distribution)
- [Layout of an Enso Version Package](#layout-of-an-enso-version-package)
- [Standard Library](#standard-library)
- [Resolvers](#resolvers)
- [Enso Home Layout](#enso-home-layout)
<!-- /MarkdownTOC -->
@ -64,19 +64,19 @@ extraction-location
│ └── 1.2.0 # A full distribution of given Enso version, described below.
│ └── <truncated>
├── runtime # A directory storing distributions of the JVM used by the Enso distributions.
│ └── graalvm-ce-27.1.1
├── lib
│ └── src # Contains sources of downloaded libraries.
│ └── Dataframe # Each library may be stored in multiple version.
│ └── graalvm-ce-java11-27.1.1
├── lib # Contains sources of downloaded libraries.
│ └── Standard # Each prefix (usually corresponding to the author) is placed in a separate directory.
│ └── Dataframe # Each library may be stored in multiple versions.
│ └── 1.7.0 # Each version contains a standard Enso package.
│ ├── package.yaml
│ └── src
│ ├── List.enso
│ ├── Number.enso
│ └── Text.enso
├── resolvers # Contains resolver specifications, described below.
│ ├── lts-1.56.7.yaml
│ └── lts-2.0.8.yaml
├── editions # Contains Edition specifications.
│ ├── 2021.4.yaml
│ └── nightly-2021-06-31.yaml
├── README.md # Information on layout and usage of the Enso distribution.
├── .enso.portable # A file that allows the universal launcher to detect that if it is run from this directory, it should run in portable distribution mode.
└── THIRD-PARTY # Contains licences of distributed components, including the NOTICE file.
@ -94,19 +94,19 @@ ENSO_DATA_DIRECTORY
│ └── 1.2.0 # A full distribution of given Enso version, described below.
│ └── <truncated>
├── runtime # A directory storing (optional) distributions of the JVM used by the Enso distributions.
│ └── graalvm-ce-27.1.1
├── lib
│ └── src # Contains sources of downloaded libraries.
│ └── Dataframe # Each library may be stored in multiple version.
│ └── graalvm-ce-java11-27.1.1
├── lib # Contains sources of downloaded libraries.
│ └── Standard # Each prefix (usually corresponding to the author) is placed in a separate directory.
│ └── Dataframe # Each library may be stored in multiple versions.
│ └── 1.7.0 # Each version contains a standard Enso package.
│ ├── package.yaml
│ └── src
│ ├── List.enso
│ ├── Number.enso
│ └── Text.enso
└── resolvers # Contains resolver specifications, described below.
├── lts-1.56.7.yaml
└── lts-2.0.8.yaml
└── editions # Contains Edition specifications.
├── 2021.4.yaml
└── nightly-2021-06-31.yaml
ENSO_CONFIG_DIRECTORY
└── global-config.yaml # Global user configuration.
@ -202,20 +202,19 @@ but the following are some guidelines:
3. Packages that the compiler relies on, e.g. compile error definitions, stack
traces etc.
### Resolvers
## Enso Home Layout
**Note** This system is not implemented yet.
The location called in some places `<ENSO_HOME>` is the place where user's
projects and similar files are stored. Currently it is specified to always be
`$HOME/enso`.
A resolver is a manifest containing library versions that are automatically
available for import in any project using the resolver.
It has the following structure:
Example contents of a resolver file are as follows:
```yaml
enso-version: 1.0.7
libraries:
- name: Base
version: 1.0.0
- name: Http
version: 5.3.5
```
<ENSO_HOME>
├── projects # Contains all user projects.
├── libraries # Contains all local libraries that can be edited by the user.
│ └── Prefix1 # Contains libraries with the given prefix.
│ └── Library_Name # Contains a package of a local library.
└── editions # Contains custom, user-defined editions that can be used as a base for project configurations.
```

View File

@ -102,19 +102,20 @@ The following is an example of this manifest file.
license: MIT
name: My_Package
version: 1.0.1
enso-version: 0.2.0
edition:
extends: 2021.3
enso-version: 0.2.12
libraries:
- name: Foo.Bar
version: 1.2.3
repository: main
prefer-local-libraries: false
authors:
- name: John Doe
email: john.doe@example.com
maintainers:
- name: Jane Doe
email: jane.doe@example.com
resolver: lts-1.2.0
extra-dependencies:
- name: Base
version: "1.2.0"
- name: Http
version: "4.5.3"
```
The following is the specification of the manifest fields. Fields marked as
@ -129,12 +130,37 @@ fields cannot be published.
package. Defaults to `None`, meaning the package is not safe for use by third
parties.
#### edition
**Optional (required for publishing)** _Edition_: Defines the Edition settings
of the package that determine the engine version and library resolution
settings. It is a sub-setting that can consist of multiple fields, see
[the Edition documentation](../libraries/editions.md#the-edition-file) for the
format description.
The field was added in version 0.2.12 as a replacement for `enso-version` as it
supersedes its functionality.
If the `edition` field is not specified, a default edition is used.
#### enso-version
**Optional (required for publishing)** _String_: Specifies the Enso version that
should be used for this project. If not set or set to `default`, the default
locally installed Enso version will be used. The version should not be `default`
if the package is to be published.
**Deprecated** _String_: Specifies the Enso version that should be used for this
project. If not set or set to `default`, the default locally installed Enso
version will be used.
The field was deprecated in version 0.2.12. Currently it is still supported, but
the newer tools will migrate it to the `edition` format when the config is
modified.
If old tools see a config file that includes an `edition` setting but does not
include the `engine-version` (for example after the migration), they will fall
back to using the default engine version - that is because old tools were not
aware of the `edition` field, so they will simply ignore it.
If a config defines the `edition` field it should not define the
`engine-version` field anymore, as that could lead to inconsistent engine
version settings.
#### version
@ -163,32 +189,18 @@ present.
**Optional** _List of contacts_: The name(s) and contact info(s) of the current
maintainer(s) of this library, in the same format as `authors` above.
#### resolver
#### prefer-local-libraries
**Note** This field is not currently implemented. **Optional (required for
publishing)** _String_: The resolver name, used to choose compiler version and
basic libraries set. If not set, the system-default resolver will be used.
**Optional** _Boolean_: A flag that tells the library resolver to prefer local
library versions over the ones specified by the edition configuration. This is
useful to make all local libraries easily accessible, but in more sophisticated
scenarios individual `local` repository overrides should be used instead of
that. See [Library Resolution](../libraries/editions.md#library-resolution) for
more details.
> The actionables for this section are:
>
> - Extend the compiler version to handle version bounds.
#### extra-dependencies
**Note** This field is not currently implemented. **Optional** _List of Library
objects_: The list of libraries this package requires to function properly and
that are not included in the resolver. Defaults to an empty list.
A library object is of the form:
```yaml
name: <name of the library>
version: <semver string of the required library version>
```
> The actionables for this section are:
>
> - Extend the library version field to handle version bounds.
If the flag is not specified, it defaults to `false`, delegating all library
resolution to the edition configuration. However, newly created projects will
have it set to `true`.
### The `visualization` Directory

14
docs/libraries/README.md Normal file
View File

@ -0,0 +1,14 @@
---
layout: section-summary
title: Enso Library Ecosystem
category: libraries
tags: [libraries, readme]
order: 0
---
# Enso Library Ecosystem
Documents in this section describe Enso's library ecosystem.
- [**Editions:**](./editions.md) Information on Editions, the concept that
organizes library versioning.

217
docs/libraries/editions.md Normal file
View File

@ -0,0 +1,217 @@
---
layout: developer-doc
title: Editions
category: libraries
tags: [libraries, editions]
order: 1
---
# Editions
This document describes the concept of Editions.
<!-- MarkdownTOC levels="2,3" autolink="true" -->
- [What Is An Edition](#what-is-an-edition)
- [The Edition File](#the-edition-file)
- [Repositories](#repositories)
- [Libraries](#libraries)
- [Extending the Editions](#extending-the-editions)
- [An Example Configuration](#an-example-configuration)
- [Edition Resolution](#edition-resolution)
- [Updating the Editions](#updating-the-editions)
- [Library Resolution](#library-resolution)
<!-- /MarkdownTOC -->
## What Is An Edition
An Edition, is in principle, a list of library versions that should be
compatible with each other. An Edition specifies the engine version and a set of
library versions that can be used together.
If a library included in an Edition depends on another library, that other
library must also be included in that Edition and the version at which it is
included must be compatible with the library that depends on it. Each Edition
may only include a single version of each library.
Thus, when a library is to be installed, its version is uniquely determined by
the selected Edition. A curated Edition file will guarantee that all libraries
are compatible which simplifies version resolution - the exact version that is
specified in the Edition is always used.
## The Edition File
The Edition file is a YAML file that can contain the following fields:
- `engine-version` which should be a semantic versioning string specifying the
engine version that should be associated with that edition,
- `repositories` which defines the repositories which are sources of library
packages, its format is [described below](#repositories),
- `extends` which can contain a name of another Edition that this Edition
extends,
- `libraries` which defines the libraries that this Edition should include, its
format is [described below](#libraries).
Every field is optional, but for an Edition file to be valid it must specify at
least the engine version to be used (either by specifying it directly or
extending another edition that specifies it).
### Repositories
The `repositories` field is a list of repository objects.
Each object must have:
- a `name` field which specifies the name under which this repository will be
referred to in the rest of the file,
- a `url` field which specifies the URL of the root of that repository.
The `name` can be any string which only needs to be consistent with the names
used in the package definitions. The only reserved name is `local`, which is a
special repository name, as [explained below](#library-resolution).
### Libraries
The `libraries` field defines the set of libraries included in the edition.
Each library is represented by an object that must have:
- a `name` field which is the fully qualified name of the library (consisting of
its prefix and the name itself),
- a `repository` field which specifies which repository this package should be
downloaded from. The `repository` field should refer to the `name` of one of
the repositories defined in the edition or to `local`,
- a `version` field which specifies which exact package version should be used
when the library is imported; it is normally required, but if the `repository`
is set to `local`, the version must not be specified as the version will only
depend on what is available in the local repository,
- an optional `hash` that can be included to verify the integrity of the
package.
The `hash` field is currently not implemented.
### Extending the Editions
An edition may extend another one by using the `extends` property specifying the
name of the edition that is to be extended. Henceforth we will call the edition
that is being extended 'the parent edition' and the other one 'the local
edition'.
The current edition inherits all configuration of the parent edition, but it can
also override specific settings.
If the `engine-version` is specified in the current edition, it overrides the
engine version that was implied from the parent edition.
If the current edition specifies its libraries, they are added to the set of
available libraries defined by the parent edition. If the current edition
defines a library that has the same fully qualified name as a library that was
already defined in the parent edition, the definition from the current edition
takes precedence. This is the most important mechanism of extending editions
that allows to override library settings.
The libraries defined in the current edition can refer to the repositories
defined both in the current edition and in the parent. However, if the current
edition defines a repository with the same name as some repository defined in
the parent edition, the definition from the current edition takes precedence for
the package definitions of the current definition, **but** the package
definitions in the parent edition are not affected (they still refer to the
definition from the their own edition). So you can shadow a repository name, but
you cannot override it for libraries from the parent edition - instead libraries
whose repository should be changed must all be overridden in the current
edition.
Extending editions can be arbitrarily nested. That is, an edition can extend
another edition that extends another one etc. The only limitation is that
obviously there can be no cycles in the chain of extensions. Multiple extensions
are resolved as follows: first the parent edition is completely resolved (which
may recursively need to first resolve its parents etc.) and only then the
current edition applies its overrides.
### An Example Configuration
```yaml
extends: 2021.4
engine-version: 1.2.3
repositories:
- name: secondary
url: https://example.com/
libraries:
- name: Foo.Bar
version: 1.0.0
repository: secondary
```
The edition file shown above extends a base edition file called `2021.4`. It
overrides the engine version set in the parent edition to `1.2.3`. Moreover it
adds a library `Foo.Bar` from the `secondary` repository, or if `2021.4`
included the library `Foo.Bar`, its definition is overridden with the one
provided here.
## Edition Resolution
The edition configuration for a project is loaded from the `edition` section in
its `package.yaml` configuration. This 'per-project' edition has no assigned
names, but it can refer to other editions by their names (when extending them).
These editions are resolved using the logic below:
1. Each `<edition-name>` corresponds to a file `<edition-name>.yaml`.
2. First, the directory `<ENSO_HOME>/editions` is scanned for a matching edition
file.
3. If none is found above, the directory `$ENSO_DATA_DIRECTORY/editions` is
checked.
See [The Enso Distribution](../distribution/distribution.md) for definitions of
the directories.
### Updating the Editions
The global user configuration file should contain a list of URLs specifying
edition providers that should be used. By default (if the field is missing), it
will default to our official edition provider, but users may add other providers
or remove the official one.
When `enso update-editions` is called or when requested by the IDE, these
providers are queried and any new edition files are downloaded to the
`$ENSO_DATA_DIRECTORY/editions` directory. Editions are assumed to be immutable,
so edition files that already exist on disk are not redownloaded.
## Library Resolution
Below are listed the steps that are taken when resolving an import of library
`Foo.Bar`:
1. If and only if the project has `prefer-local-libraries` set to `true` and if
any directory on the library path contains `Foo/Bar`, that local instance is
chosen as the library that should be used, regardless of the version that is
there;
2. Otherwise, the list of libraries defined directly in the `edition` section of
`package.yaml` of the current project is checked, and if the library is
defined there, it is selected.
3. Otherwise, any parent editions are consulted; if they too do not contain the
library that we are searching for, an error is reported.
4. Once we know the library version to be used:
1. If the repository associated with the library is `local`, the library path
is searched for the first directory to contain `Foo/Bar` and this path is
loaded. If the library is not present on the library path, an error is
reported.
2. Otherwise, the edition must have defined an exact `<version>` of the
library that is supposed to be used.
3. If the library is already downloaded in the local repository cache, that
is the directory `$ENSO_DATA_DIRECTORY/lib/Foo/Bar/<version>` exists, that
package is loaded.
4. Otherwise, the library is missing and must be downloaded from its
associated repository (and placed in the cache as above).
By default, the library path is `<ENSO_HOME>/libraries/` but it can be
overridden by setting the `ENSO_LIBRARY_PATH` environment variable. It may
include a list of directories (separated by the system specific path separator);
the first directory on the list has the highest precedence.
In particular, if `prefer-local-libraries` is `false`, and the edition does not
define a library at all, when trying to resolve such a library, it is reported
as not found even if a local version of it exists. That is because
auto-discovery of local libraries is only done with `prefer-local-libraries` set
to `true`. In all other cases, the `local` repository overrides should be set
explicitly.

View File

@ -1,10 +1,10 @@
package org.enso.launcher
import java.nio.file.Path
import com.typesafe.scalalogging.Logger
import io.circe.Json
import nl.gn0s1s.bump.SemVer
import org.enso.distribution.EditionManager
import org.enso.runtimeversionmanager.CurrentVersion
import org.enso.runtimeversionmanager.config.{
DefaultVersion,
@ -42,12 +42,14 @@ case class Launcher(cliOptions: GlobalCLIOptions) {
runtimeVersionManager(cliOptions, alwaysInstallMissing = false)
private lazy val configurationManager =
new GlobalConfigurationManager(componentsManager, distributionManager)
private lazy val projectManager = new ProjectManager(configurationManager)
private lazy val editionManager = EditionManager(distributionManager)
private lazy val projectManager = new ProjectManager
private lazy val runner =
new LauncherRunner(
projectManager,
configurationManager,
componentsManager,
editionManager,
LauncherEnvironment,
LauncherLogging.loggingServiceEndpoint()
)

View File

@ -3,12 +3,12 @@ package org.enso.launcher.cli
import com.typesafe.scalalogging.Logger
import nl.gn0s1s.bump.SemVer
import org.enso.cli.CLIOutput
import org.enso.distribution.locking.Resource
import org.enso.launcher.InfoLogger
import org.enso.runtimeversionmanager.components.{
GraalVMVersion,
RuntimeVersionManagementUserInterface
}
import org.enso.runtimeversionmanager.locking.Resource
/** [[RuntimeVersionManagementUserInterface]] that reports information and progress
* to the command line.

View File

@ -2,13 +2,13 @@ package org.enso.launcher.cli
import java.io.IOException
import java.nio.file.{Files, NoSuchFileException, Path}
import cats.implicits._
import nl.gn0s1s.bump.SemVer
import org.enso.cli.arguments.Opts
import org.enso.cli.arguments.Opts.implicits._
import org.enso.runtimeversionmanager.{CurrentVersion, FileSystem, OS}
import org.enso.runtimeversionmanager.FileSystem.PathSyntax
import org.enso.distribution.{FileSystem, OS}
import org.enso.runtimeversionmanager.CurrentVersion
import org.enso.distribution.FileSystem.PathSyntax
import org.enso.runtimeversionmanager.cli.Arguments._
import org.enso.launcher.distribution.LauncherEnvironment
import org.enso.launcher.upgrade.LauncherUpgrader

View File

@ -1,10 +1,9 @@
package org.enso.launcher.cli
import java.nio.file.{Files, Path}
import org.enso.cli.arguments
import org.enso.cli.arguments.CommandHelp
import org.enso.runtimeversionmanager.{Environment, FileSystem}
import org.enso.distribution.{Environment, FileSystem}
import org.enso.launcher.distribution.LauncherEnvironment
import scala.sys.process._

View File

@ -1,22 +1,15 @@
package org.enso.launcher.components
import java.nio.file.{Files, Path}
import akka.http.scaladsl.model.Uri
import nl.gn0s1s.bump.SemVer
import org.enso.runtimeversionmanager.Environment
import org.enso.runtimeversionmanager.components.RuntimeVersionManager
import org.enso.runtimeversionmanager.config.GlobalConfigurationManager
import org.enso.runtimeversionmanager.runner.{
LanguageServerOptions,
RunSettings,
Runner,
RunnerError,
WhichEngine
}
import org.enso.distribution.{EditionManager, Environment}
import org.enso.launcher.project.ProjectManager
import org.enso.loggingservice.LogLevel
import org.enso.runtimeversionmanager.components.RuntimeVersionManager
import org.enso.runtimeversionmanager.config.GlobalConfigurationManager
import org.enso.runtimeversionmanager.runner._
import java.nio.file.{Files, Path}
import scala.concurrent.Future
import scala.util.Try
@ -26,10 +19,13 @@ class LauncherRunner(
projectManager: ProjectManager,
configurationManager: GlobalConfigurationManager,
componentsManager: RuntimeVersionManager,
editionManager: EditionManager,
environment: Environment,
loggerConnection: Future[Option[Uri]]
) extends Runner(
componentsManager,
configurationManager,
editionManager,
environment,
loggerConnection
) {
@ -53,12 +49,7 @@ class LauncherRunner(
projectManager.findProject(currentWorkingDirectory).get
}
val version =
versionOverride.getOrElse {
inProject
.map(_.version)
.getOrElse(configurationManager.defaultVersion)
}
val version = resolveVersion(versionOverride, inProject)
val arguments = inProject match {
case Some(project) =>
val projectPackagePath =
@ -110,9 +101,7 @@ class LauncherRunner(
val project =
if (projectMode) Some(projectManager.loadProject(actualPath).get)
else projectManager.findProject(actualPath).get
val version = versionOverride
.orElse(project.map(_.version))
.getOrElse(configurationManager.defaultVersion)
val version = resolveVersion(versionOverride, project)
val arguments =
if (projectMode) Seq("--run", actualPath.toString)
@ -184,8 +173,7 @@ class LauncherRunner(
for {
project <- projectManager.findProject(currentWorkingDirectory)
} yield {
val version =
project.map(_.version).getOrElse(configurationManager.defaultVersion)
val version = resolveVersion(None, project)
val arguments =
Seq("--version") ++ (if (useJSON) Seq("--json") else Seq())

View File

@ -1,5 +1,10 @@
package org.enso.launcher.distribution
import org.enso.distribution.PortableDistributionManager
import org.enso.distribution.locking.{
ResourceManager,
ThreadSafeFileLockManager
}
import org.enso.launcher.cli.{
CLIRuntimeVersionManagementUserInterface,
GlobalCLIOptions
@ -11,14 +16,7 @@ import org.enso.runtimeversionmanager.components.{
RuntimeComponentUpdaterFactory,
RuntimeVersionManager
}
import org.enso.runtimeversionmanager.distribution.{
PortableDistributionManager,
TemporaryDirectoryManager
}
import org.enso.runtimeversionmanager.locking.{
ResourceManager,
ThreadSafeFileLockManager
}
import org.enso.runtimeversionmanager.distribution.TemporaryDirectoryManager
import org.enso.runtimeversionmanager.releases.engine.EngineRepository
import org.enso.runtimeversionmanager.releases.graalvm.GraalCEReleaseProvider

View File

@ -1,9 +1,8 @@
package org.enso.launcher.distribution
import java.nio.file.Path
import com.typesafe.scalalogging.Logger
import org.enso.runtimeversionmanager.Environment
import org.enso.distribution.Environment
/** Default [[Environment]] to use in the launcher.
*

View File

@ -1,14 +1,17 @@
package org.enso.launcher.installation
import java.nio.file.{Files, Path}
import com.typesafe.scalalogging.Logger
import org.enso.cli.CLIOutput
import org.enso.runtimeversionmanager.FileSystem.PathSyntax
import org.enso.distribution.{
DistributionManager,
FileSystem,
OS,
PortableDistributionManager
}
import org.enso.distribution.locking.ResourceManager
import org.enso.distribution.FileSystem.PathSyntax
import org.enso.runtimeversionmanager.config.GlobalConfigurationManager
import org.enso.runtimeversionmanager.distribution.PortableDistributionManager
import org.enso.runtimeversionmanager.locking.ResourceManager
import org.enso.runtimeversionmanager.{FileSystem, OS}
import org.enso.launcher.InfoLogger
import org.enso.launcher.cli.{GlobalCLIOptions, InternalOpts, Main}
import org.enso.launcher.distribution.DefaultManagers
@ -54,9 +57,9 @@ class DistributionInstaller(
private val nonEssentialDirectories = Seq("THIRD-PARTY")
private val enginesDirectory =
installed.dataDirectory / manager.ENGINES_DIRECTORY
installed.dataDirectory / DistributionManager.ENGINES_DIRECTORY
private val runtimesDirectory =
installed.dataDirectory / manager.RUNTIMES_DIRECTORY
installed.dataDirectory / DistributionManager.RUNTIMES_DIRECTORY
/** Installs the distribution under configured location.
*

View File

@ -1,15 +1,18 @@
package org.enso.launcher.installation
import java.nio.file.{Files, Path}
import com.typesafe.scalalogging.Logger
import org.apache.commons.io.FileUtils
import org.enso.cli.CLIOutput
import org.enso.runtimeversionmanager.FileSystem.PathSyntax
import org.enso.distribution.{
DistributionManager,
FileSystem,
OS,
PortableDistributionManager
}
import org.enso.distribution.locking.ResourceManager
import org.enso.distribution.FileSystem.PathSyntax
import org.enso.runtimeversionmanager.config.GlobalConfigurationManager
import org.enso.runtimeversionmanager.distribution.PortableDistributionManager
import org.enso.runtimeversionmanager.locking.ResourceManager
import org.enso.runtimeversionmanager.{FileSystem, OS}
import org.enso.launcher.InfoLogger
import org.enso.launcher.cli.{
GlobalCLIOptions,
@ -178,7 +181,7 @@ class DistributionUninstaller(
private val knownDataDirectories =
Set.from(
manager.LocallyInstalledDirectories.possibleDirectoriesInsideDataDirectory
) - manager.LOCK_DIRECTORY
) - DistributionManager.LOCK_DIRECTORY
/** Removes all files contained in the ENSO_DATA_DIRECTORY and possibly the
* directory itself.
@ -211,7 +214,7 @@ class DistributionUninstaller(
resourceManager.unlockTemporaryDirectory()
resourceManager.releaseMainLock()
val lockDirectory = dataRoot / manager.LOCK_DIRECTORY
val lockDirectory = dataRoot / DistributionManager.LOCK_DIRECTORY
if (Files.isDirectory(lockDirectory)) {
for (lock <- FileSystem.listDirectory(lockDirectory)) {
try {

View File

@ -1,11 +1,9 @@
package org.enso.launcher.project
import java.nio.file.Path
import org.enso.runtimeversionmanager.config.GlobalConfigurationManager
import org.enso.runtimeversionmanager.runner.Project
import org.enso.pkg.PackageManager
import org.enso.runtimeversionmanager.runner.Project
import java.nio.file.Path
import scala.util.{Failure, Try}
/** A helper class for project management.
@ -13,7 +11,7 @@ import scala.util.{Failure, Try}
* It allows to create new project, open existing ones or traverse the
* directory tree to find a project based on a path inside it.
*/
class ProjectManager(globalConfigurationManager: GlobalConfigurationManager) {
class ProjectManager {
private val packageManager = PackageManager.Default
@ -22,7 +20,7 @@ class ProjectManager(globalConfigurationManager: GlobalConfigurationManager) {
def loadProject(path: Path): Try[Project] =
packageManager
.loadPackage(path.toFile)
.map(new Project(_, globalConfigurationManager))
.map(new Project(_))
.recoverWith(error => Failure(ProjectLoadingError(path, error)))
/** Traverses the directory tree looking for a project in one of the ancestors
@ -40,7 +38,7 @@ class ProjectManager(globalConfigurationManager: GlobalConfigurationManager) {
private def tryFindingProject(root: Path): Try[Project] =
packageManager
.loadPackage(root.toFile)
.map(new Project(_, globalConfigurationManager))
.map(new Project(_))
.recoverWith {
case PackageManager.PackageNotFound() if root.getParent != null =>
tryFindingProject(root.getParent)

View File

@ -3,7 +3,7 @@ package org.enso.launcher.releases.launcher
import io.circe.{yaml, Decoder}
import nl.gn0s1s.bump.SemVer
import org.enso.runtimeversionmanager.releases.ReleaseProviderException
import org.enso.pkg.SemVerJson._
import org.enso.editions.SemVerJson._
import scala.util.{Failure, Try}

View File

@ -1,32 +1,32 @@
package org.enso.launcher.upgrade
import java.nio.file.{Files, Path}
import com.typesafe.scalalogging.Logger
import nl.gn0s1s.bump.SemVer
import org.enso.cli.CLIOutput
import org.enso.runtimeversionmanager.{CurrentVersion, FileSystem, OS}
import org.enso.runtimeversionmanager.FileSystem.PathSyntax
import org.enso.runtimeversionmanager.archive.Archive
import org.enso.runtimeversionmanager.components.UpgradeRequiredError
import org.enso.runtimeversionmanager.distribution.DistributionManager
import org.enso.launcher.cli.{
CLIProgressReporter,
GlobalCLIOptions,
InternalOpts
}
import org.enso.runtimeversionmanager.locking.{
import org.enso.distribution.{DistributionManager, FileSystem, OS}
import org.enso.distribution.locking.{
LockType,
LockUserInterface,
Resource,
ResourceManager
}
import org.enso.runtimeversionmanager.CurrentVersion
import org.enso.distribution.FileSystem.PathSyntax
import org.enso.runtimeversionmanager.archive.Archive
import org.enso.runtimeversionmanager.components.UpgradeRequiredError
import org.enso.launcher.cli.{
CLIProgressReporter,
GlobalCLIOptions,
InternalOpts
}
import org.enso.launcher.releases.launcher.LauncherRelease
import org.enso.runtimeversionmanager.releases.ReleaseProvider
import org.enso.launcher.releases.LauncherRepository
import org.enso.launcher.InfoLogger
import org.enso.launcher.distribution.DefaultManagers
import org.enso.logger.LoggerSyntax
import org.enso.runtimeversionmanager.locking.Resources
import scala.util.Try
import scala.util.control.NonFatal
@ -66,7 +66,7 @@ class LauncherUpgrader(
}
resourceManager.withResource(
failIfAnotherUpgradeIsRunning,
Resource.LauncherExecutable,
Resources.LauncherExecutable,
LockType.Exclusive
) {
runCleanup(isStartup = true)

View File

@ -1,8 +1,8 @@
package org.enso.launcher
import java.nio.file.{Files, Path}
import org.enso.distribution.OS
import org.enso.runtimeversionmanager.OS
import java.nio.file.{Files, Path}
import org.enso.runtimeversionmanager.test.NativeTestHelper
import org.scalatest.concurrent.{Signaler, TimeLimitedTests}
import org.scalatest.matchers.should.Matchers

View File

@ -4,7 +4,8 @@ import java.nio.file.{Files, Path}
import java.util.UUID
import akka.http.scaladsl.model.Uri
import nl.gn0s1s.bump.SemVer
import org.enso.runtimeversionmanager.FileSystem.PathSyntax
import org.enso.distribution.EditionManager
import org.enso.distribution.FileSystem.PathSyntax
import org.enso.runtimeversionmanager.config.GlobalConfigurationManager
import org.enso.runtimeversionmanager.runner._
import org.enso.runtimeversionmanager.test.RuntimeVersionManagerTest
@ -30,13 +31,15 @@ class LauncherRunnerSpec extends RuntimeVersionManagerTest {
new GlobalConfigurationManager(componentsManager, distributionManager) {
override def defaultVersion: SemVer = defaultEngineVersion
}
val projectManager = new ProjectManager(configurationManager)
val editionManager = EditionManager(distributionManager)
val projectManager = new ProjectManager()
val cwd = cwdOverride.getOrElse(getTestDirectory)
val runner =
new LauncherRunner(
projectManager,
configurationManager,
componentsManager,
editionManager,
env,
Future.successful(Some(fakeUri))
) {

View File

@ -1,9 +1,9 @@
package org.enso.launcher.installation
import java.nio.file.{Files, Path}
import org.enso.distribution.{FileSystem, OS}
import org.enso.runtimeversionmanager.{FileSystem, OS}
import org.enso.runtimeversionmanager.FileSystem.PathSyntax
import java.nio.file.{Files, Path}
import FileSystem.PathSyntax
import org.enso.runtimeversionmanager.test.WithTemporaryDirectory
import org.enso.launcher._

View File

@ -1,9 +1,9 @@
package org.enso.launcher.installation
import java.nio.file.{Files, Path}
import org.enso.distribution.{FileSystem, OS}
import org.enso.runtimeversionmanager.{FileSystem, OS}
import org.enso.runtimeversionmanager.FileSystem.PathSyntax
import java.nio.file.{Files, Path}
import FileSystem.PathSyntax
import org.enso.runtimeversionmanager.test.WithTemporaryDirectory
import org.enso.launcher.NativeTest

View File

@ -1,8 +1,8 @@
package org.enso.launcher.project
import nl.gn0s1s.bump.SemVer
import org.enso.distribution.DistributionManager
import org.enso.runtimeversionmanager.config.GlobalConfigurationManager
import org.enso.runtimeversionmanager.distribution.DistributionManager
import org.enso.runtimeversionmanager.test.RuntimeVersionManagerTest
import org.enso.pkg.Contact
import org.scalatest.{Inside, OptionValues}
@ -19,7 +19,7 @@ class ProjectManagerSpec
new GlobalConfigurationManager(null, distributionManager) {
override def defaultVersion: SemVer = defaultEnsoVersion
}
(fakeConfigurationManager, new ProjectManager(fakeConfigurationManager))
(fakeConfigurationManager, new ProjectManager())
}
"ProjectManager" should {
@ -43,7 +43,8 @@ class ProjectManagerSpec
projectDir.resolve("src").resolve("Main.enso").toFile should exist
val project = projectManager.loadProject(projectDir).get
project.version shouldEqual defaultEnsoVersion
// TODO [RW] this test may change once we switch to deriving a particular edition by default
project.edition.engineVersion should contain(defaultEnsoVersion)
project.config.authors.headOption.value shouldEqual author
project.config.maintainers.headOption.value shouldEqual author
}

View File

@ -1,8 +1,8 @@
package org.enso.launcher.releases.fallback
import java.nio.file.Path
import org.enso.distribution.FileSystem
import org.enso.runtimeversionmanager.FileSystem
import java.nio.file.Path
import org.enso.launcher.TestHelpers
import org.enso.launcher.releases.fallback.staticwebsite.FileStorageFallbackReleaseProvider
import org.scalatest.matchers.should.Matchers

View File

@ -1,9 +1,8 @@
package org.enso.launcher.releases.fallback
import java.nio.file.Path
import org.enso.cli.task.{TaskProgress, TaskProgressImplementation}
import org.enso.runtimeversionmanager.FileSystem
import org.enso.distribution.FileSystem
import org.enso.launcher.TestHelpers
import org.enso.launcher.releases.fallback.staticwebsite.FileStorage

View File

@ -1,13 +1,12 @@
package org.enso.launcher.upgrade
import java.nio.file.{Files, Path, StandardCopyOption}
import io.circe.parser
import nl.gn0s1s.bump.SemVer
import org.enso.runtimeversionmanager.{FileSystem, OS}
import org.enso.runtimeversionmanager.FileSystem.PathSyntax
import org.enso.distribution.{FileSystem, OS}
import org.enso.distribution.locking.{FileLockManager, LockType}
import FileSystem.PathSyntax
import org.enso.launcher._
import org.enso.runtimeversionmanager.locking.{FileLockManager, LockType}
import org.enso.runtimeversionmanager.test.WithTemporaryDirectory
import org.enso.testkit.RetrySpec
import org.scalatest.exceptions.TestFailedException

View File

@ -4,7 +4,7 @@ import org.enso.polyglot.debugger.protocol.{
ExceptionRepresentation,
ObjectRepresentation
}
import org.enso.testkit.EitherValue
import org.scalatest.matchers.should.Matchers
import org.scalatest.wordspec.AnyWordSpec

View File

@ -3,10 +3,11 @@ package org.enso.runner
import akka.http.scaladsl.model.{IllegalUriException, Uri}
import cats.implicits._
import org.apache.commons.cli.{Option => CliOption, _}
import org.enso.editions.SemVerEnsoVersion
import org.enso.languageserver.boot
import org.enso.languageserver.boot.LanguageServerConfig
import org.enso.loggingservice.LogLevel
import org.enso.pkg.{Contact, PackageManager, SemVerEnsoVersion}
import org.enso.pkg.{Contact, PackageManager}
import org.enso.polyglot.{LanguageInfo, Module, PolyglotContext}
import org.enso.version.VersionDescription
import org.graalvm.polyglot.PolyglotException

View File

@ -1,11 +1,9 @@
package org.enso.runtimeversionmanager.distribution
import java.nio.file.{Files, Path}
package org.enso.distribution
import com.typesafe.scalalogging.Logger
import org.enso.runtimeversionmanager.FileSystem.PathSyntax
import org.enso.runtimeversionmanager.{Environment, FileSystem, OS}
import org.enso.distribution.FileSystem.PathSyntax
import java.nio.file.{Files, Path}
import scala.util.Try
import scala.util.control.NonFatal
@ -27,6 +25,10 @@ import scala.util.control.NonFatal
* @param unsafeTemporaryDirectory path to the temporary directory, should not
* be used directly, see
* [[TemporaryDirectoryManager]]
* @param customEditions the search paths for editions
* @param localLibrariesSearchPaths a sequence of paths to search for local
* libraries, in order of precedence
* @param ensoHome the home directory for user's projects etc.
*/
case class DistributionPaths(
dataRoot: Path,
@ -36,7 +38,10 @@ case class DistributionPaths(
config: Path,
locks: Path,
logs: Path,
unsafeTemporaryDirectory: Path
unsafeTemporaryDirectory: Path,
customEditions: Seq[Path],
localLibrariesSearchPaths: Seq[Path],
ensoHome: Path
) {
/** @inheritdoc */
@ -48,7 +53,8 @@ case class DistributionPaths(
| bundle = $bundle,
| config = $config,
| locks = $locks,
| tmp = $unsafeTemporaryDirectory
| tmp = $unsafeTemporaryDirectory,
| ensoHome = $ensoHome
|)""".stripMargin
/** Sequence of paths to search for engine installations, in order of
@ -61,6 +67,18 @@ case class DistributionPaths(
*/
def runtimeSearchPaths: Seq[Path] =
Seq(runtimes) ++ bundle.map(_.runtimes).toSeq
/** The directory for cached editions managed by us. */
def cachedEditions: Path = dataRoot / DistributionManager.EDITIONS_DIRECTORY
/** The directory for cached libraries managed by us. */
def cachedLibraries: Path = dataRoot / DistributionManager.LIBRARIES_DIRECTORY
/** Sequence of paths to search for edition configurations, in order of
* precedence.
*/
def editionSearchPaths: Seq[Path] =
customEditions ++ Seq(cachedEditions)
}
/** Paths to secondary directories for additionally bundled engine
@ -84,6 +102,7 @@ case class Bundle(engines: Path, runtimes: Path)
*/
class DistributionManager(val env: Environment) {
private val logger = Logger[DistributionManager]
import DistributionManager._
/** Determines paths that should be used by the launcher.
*/
@ -93,30 +112,56 @@ class DistributionManager(val env: Environment) {
paths
}
val ENGINES_DIRECTORY = "dist"
val RUNTIMES_DIRECTORY = "runtime"
val CONFIG_DIRECTORY = "config"
val BIN_DIRECTORY = "bin"
val LOCK_DIRECTORY = "lock"
val LOG_DIRECTORY = "log"
val TMP_DIRECTORY = "tmp"
protected def detectPaths(): DistributionPaths = {
val dataRoot = LocallyInstalledDirectories.dataDirectory
val configRoot = LocallyInstalledDirectories.configDirectory
val runRoot = LocallyInstalledDirectories.runtimeDirectory
val home = detectEnsoHome()
DistributionPaths(
dataRoot = dataRoot,
runtimes = dataRoot / RUNTIMES_DIRECTORY,
engines = dataRoot / ENGINES_DIRECTORY,
bundle = detectBundle(),
config = configRoot,
locks = runRoot / LOCK_DIRECTORY,
logs = LocallyInstalledDirectories.logDirectory,
unsafeTemporaryDirectory = dataRoot / TMP_DIRECTORY
dataRoot = dataRoot,
runtimes = dataRoot / RUNTIMES_DIRECTORY,
engines = dataRoot / ENGINES_DIRECTORY,
bundle = detectBundle(),
config = configRoot,
locks = runRoot / LOCK_DIRECTORY,
logs = LocallyInstalledDirectories.logDirectory,
unsafeTemporaryDirectory = dataRoot / TMP_DIRECTORY,
customEditions = detectCustomEditionPaths(home),
localLibrariesSearchPaths = detectLocalLibraryPaths(home),
ensoHome = home
)
}
private val ENSO_HOME = "ENSO_HOME"
private val ENSO_EDITION_PATH = "ENSO_EDITION_PATH"
private val ENSO_LIBRARY_PATH = "ENSO_LIBRARY_PATH"
/** Finds the path to the ENSO_HOME directory that is used for keeping user's
* projects, libraries and other custom artifacts.
*/
protected def detectEnsoHome(): Path =
env.getEnvPath(ENSO_HOME).getOrElse(env.getUserProfile / "enso")
/** Finds the paths to look for custom editions, which may be overridden by
* setting the ENSO_EDITION_PATH environment variable.
*/
protected def detectCustomEditionPaths(ensoHome: Path): Seq[Path] =
env
.getEnvPaths(ENSO_EDITION_PATH)
.getOrElse {
Seq(ensoHome / DistributionManager.Home.EDITIONS_DIRECTORY)
}
/** Finds the paths to look for local libraries, which may be overridden by
* setting the ENSO_LIBRARY_PATH environment variable.
*/
protected def detectLocalLibraryPaths(ensoHome: Path): Seq[Path] =
env
.getEnvPaths(ENSO_LIBRARY_PATH)
.getOrElse {
Seq(ensoHome / DistributionManager.Home.LIBRARIES_DIRECTORY)
}
/** Name of the file that should be placed in the distribution root to mark it
* as running in portable mode.
*/
@ -315,3 +360,24 @@ class DistributionManager(val env: Environment) {
safeDataDirectory.exists(Files.isDirectory(_))
}
}
/** A helper object that contains constants defining names of various
* directories used by Enso components.
*/
object DistributionManager {
val ENGINES_DIRECTORY = "dist"
val RUNTIMES_DIRECTORY = "runtime"
val CONFIG_DIRECTORY = "config"
val BIN_DIRECTORY = "bin"
val LOCK_DIRECTORY = "lock"
val LOG_DIRECTORY = "log"
val TMP_DIRECTORY = "tmp"
val EDITIONS_DIRECTORY = "editions"
val LIBRARIES_DIRECTORY = "lib"
/** Defines paths inside of the ENSO_HOME directory. */
object Home {
val EDITIONS_DIRECTORY = "editions"
val LIBRARIES_DIRECTORY = "libraries"
}
}

View File

@ -0,0 +1,37 @@
package org.enso.distribution
import org.enso.editions
import org.enso.editions.provider.FileSystemEditionProvider
import org.enso.editions.{EditionResolver, Editions, EnsoVersion}
import scala.util.Try
/** A helper class for resolving editions backed by the Edition storage managed
* by the DistributionManager.
*/
case class EditionManager(distributionManager: DistributionManager) {
private val editionProvider = FileSystemEditionProvider(
distributionManager.paths.editionSearchPaths.toList
)
private val editionResolver = EditionResolver(editionProvider)
private val engineVersionResolver =
editions.EngineVersionResolver(editionProvider)
/** Resolves a raw edition, loading its parents from the edition search path.
* @param edition the edition to resolve
* @return the resolved edition
*/
def resolveEdition(
edition: Editions.RawEdition
): Try[Editions.ResolvedEdition] =
editionResolver.resolve(edition).toTry
/** Resolves the engine version that should be used based on the provided raw
* edition configuration.
* @param edition the edition configuration to base the selected version on
* @return the resolved engine version
*/
def resolveEngineVersion(edition: Editions.RawEdition): Try[EnsoVersion] =
engineVersionResolver.resolveEnsoVersion(edition).toTry
}

View File

@ -1,11 +1,10 @@
package org.enso.runtimeversionmanager
import java.io.File
import java.nio.file.Path
package org.enso.distribution
import com.typesafe.scalalogging.Logger
import org.enso.logger.masking.MaskedString
import java.io.File
import java.nio.file.Path
import scala.util.Try
/** Gathers some helper methods querying the system environment.
@ -39,26 +38,38 @@ trait Environment {
*
* If it is not defined or is not a valid path, returns None.
*/
def getEnvPath(key: String): Option[Path] = {
def parsePathWithWarning(str: String): Option[Path] = {
val result = safeParsePath(str)
if (result.isEmpty) {
Logger[Environment].warn(
"System variable [{}] was set to [{}], but it did not " +
"represent a valid path, so it has been ignored.",
key,
MaskedString(str)
)
def getEnvPath(key: String): Option[Path] =
getEnvVar(key).flatMap(parsePathWithWarning(key))
/** Queries the system environment for the given variable that should
* represent a list of valid filesystem paths.
*
* If a path is on the list but is invalid, it is skipped.
*/
def getEnvPaths(key: String): Option[Seq[Path]] =
getEnvVar(key)
.map { value =>
value
.split(File.pathSeparator)
.toSeq
.flatMap(str => parsePathWithWarning(key)(str).toSeq)
}
result
private def parsePathWithWarning(key: String)(str: String): Option[Path] = {
val result = safeParsePath(str)
if (result.isEmpty) {
Logger[Environment].warn(
"System variable [{}] was set to [{}], but it did not " +
"represent a valid path, so it has been ignored.",
key,
MaskedString(str)
)
}
getEnvVar(key).flatMap(parsePathWithWarning)
result
}
/** Returns the system PATH, if available.
*/
/** Returns the system PATH, if available. */
def getSystemPath: Seq[Path] =
getEnvVar("PATH")
.map(_.split(File.pathSeparatorChar).toSeq.flatMap(safeParsePath))
@ -85,6 +96,33 @@ trait Environment {
}
}
/** Returns the location of the user profile directory (`%UserProfile%`) on
* Windows.
*/
def getWindowsUserProfile: Path = {
if (!OS.isWindows)
throw new IllegalStateException(
"fatal error: USERPROFILE should be queried only on Windows."
)
else {
getEnvVar("USERPROFILE").flatMap(safeParsePath) match {
case Some(path) => path
case None =>
throw new RuntimeException(
"fatal error: %USERPROFILE% environment variable is not defined."
)
}
}
}
/** Returns a path to the user's home directory, as defined on their
* particular Operating System.
*
* On UNIX, this is $HOME and on Windows it is %UserProfile%.
*/
def getUserProfile: Path =
if (OS.isWindows) getWindowsUserProfile else getHome
/** Returns the location of the local application data directory
* (`%LocalAppData%`) on Windows.
*

View File

@ -1,4 +1,7 @@
package org.enso.runtimeversionmanager
package org.enso.distribution
import com.typesafe.scalalogging.Logger
import org.apache.commons.io.FileUtils
import java.io.PrintWriter
import java.nio.file.attribute.PosixFilePermissions
@ -8,10 +11,6 @@ import java.nio.file.{
Path,
StandardCopyOption
}
import com.typesafe.scalalogging.Logger
import org.apache.commons.io.FileUtils
import scala.collection.Factory
import scala.jdk.StreamConverters._
import scala.util.Using

View File

@ -1,4 +1,4 @@
package org.enso.runtimeversionmanager
package org.enso.distribution
import com.typesafe.scalalogging.Logger
import io.circe.{Decoder, DecodingFailure}

View File

@ -1,10 +1,9 @@
package org.enso.runtimeversionmanager.distribution
import java.nio.file.{Files, Path}
package org.enso.distribution
import com.typesafe.scalalogging.Logger
import org.enso.runtimeversionmanager.Environment
import org.enso.runtimeversionmanager.FileSystem.PathSyntax
import org.enso.distribution.FileSystem.PathSyntax
import java.nio.file.{Files, Path}
/** A specialized variant of [[DistributionManager]] that is able to detect if
* the currently running distribution is running in portable or locally
@ -69,15 +68,20 @@ class PortableDistributionManager(env: Environment)
override protected def detectPaths(): DistributionPaths =
if (isRunningPortable) {
val root = env.getPathToRunningExecutable.getParent.getParent
val home = detectEnsoHome()
import DistributionManager._
DistributionPaths(
dataRoot = root,
runtimes = root / RUNTIMES_DIRECTORY,
engines = root / ENGINES_DIRECTORY,
bundle = None,
config = root / CONFIG_DIRECTORY,
locks = root / LOCK_DIRECTORY,
logs = root / LOG_DIRECTORY,
unsafeTemporaryDirectory = root / TMP_DIRECTORY
dataRoot = root,
runtimes = root / RUNTIMES_DIRECTORY,
engines = root / ENGINES_DIRECTORY,
bundle = None,
config = root / CONFIG_DIRECTORY,
locks = root / LOCK_DIRECTORY,
logs = root / LOG_DIRECTORY,
unsafeTemporaryDirectory = root / TMP_DIRECTORY,
customEditions = detectCustomEditionPaths(home),
localLibrariesSearchPaths = detectLocalLibraryPaths(home),
ensoHome = home
)
} else super.detectPaths()

View File

@ -1,4 +1,4 @@
package org.enso.runtimeversionmanager.locking
package org.enso.distribution.locking
import java.nio.channels.{FileChannel, FileLock}
import java.nio.file.{Files, Path, StandardOpenOption}

View File

@ -1,4 +1,4 @@
package org.enso.runtimeversionmanager.locking
package org.enso.distribution.locking
import scala.util.Using.Releasable

View File

@ -1,4 +1,4 @@
package org.enso.runtimeversionmanager.locking
package org.enso.distribution.locking
/** Manages locks that can be used to synchronize different launcher processes
* running in parallel to avoid components corruption caused by simultaneous

View File

@ -1,4 +1,4 @@
package org.enso.runtimeversionmanager.locking
package org.enso.distribution.locking
/** Defines the lock type.
*/

View File

@ -1,4 +1,4 @@
package org.enso.runtimeversionmanager.locking
package org.enso.distribution.locking
/** Defines callbacks that can be called when a requested resource is locked and
* the application has to wait for other processes.

View File

@ -0,0 +1,16 @@
package org.enso.distribution.locking
/** Represents a resource that can be locked. */
trait Resource {
/** Name of the resource.
*
* Must be a valid filename part.
*/
def name: String
/** A message that is displayed by default if the lock on that resource cannot
* be acquired immediately.
*/
def waitMessage: String
}

View File

@ -1,4 +1,4 @@
package org.enso.runtimeversionmanager.locking
package org.enso.distribution.locking
import com.typesafe.scalalogging.Logger

View File

@ -0,0 +1,65 @@
package org.enso.editions
import cats.Show
/** Indicates an error during resolution of a raw edition. */
sealed class EditionResolutionError(message: String, cause: Throwable = null)
extends RuntimeException(message, cause)
object EditionResolutionError {
/** Indicates that a parent edition referenced in one of the editions that are
* being loaded cannot be loaded.
*/
case class CannotLoadEdition(name: String, cause: Throwable)
extends EditionResolutionError(
s"Cannot load the edition: ${cause.getMessage}",
cause
)
/** Indicates that the edition cannot be parsed. */
case class EditionParseError(message: String, cause: Throwable)
extends EditionResolutionError(message, cause)
/** Indicates that a library defined in an edition references a repository
* that is not defined in that edition or any of its parents, and so such a
* reference is invalid.
*/
case class LibraryReferencesUndefinedRepository(
libraryName: String,
repositoryName: String
) extends EditionResolutionError(
s"A library `$libraryName` references a repository `$repositoryName` " +
s"that is not defined in the edition or its parents."
)
/** Indicates that the chain of parent editions forms a cycle which is not
* allowed.
*/
case class EditionResolutionCycle(editions: List[String])
extends EditionResolutionError(
s"Edition resolution encountered a cycle: ${editions.mkString(" -> ")}"
)
/** Wraps a Circe's decoding error into a more user-friendly error. */
def wrapDecodingError(decodingError: io.circe.Error): EditionParseError = {
val errorMessage =
implicitly[Show[io.circe.Error]].show(decodingError)
EditionParseError(
s"Could not parse the edition: $errorMessage",
decodingError
)
}
/** Wraps a general error thrown when loading a parsing an edition into a more
* specific error type.
*/
def wrapLoadingError(
editionName: String,
throwable: Throwable
): EditionResolutionError =
throwable match {
case decodingError: io.circe.Error => wrapDecodingError(decodingError)
case other => CannotLoadEdition(editionName, other)
}
}

View File

@ -0,0 +1,155 @@
package org.enso.editions
import cats.implicits._
import org.enso.editions.EditionResolutionError.{
EditionResolutionCycle,
LibraryReferencesUndefinedRepository
}
import org.enso.editions.Editions.{RawEdition, ResolvedEdition}
import org.enso.editions.provider.EditionProvider
import scala.annotation.tailrec
/** A helper class that allows to resolve a [[RawEdition]] into a
* [[ResolvedEdition]] by loading its parents and resolving the repository
* references.
*
* @param provider an [[EditionProvider]] that is used for loading the
* referenced parent editions
*/
case class EditionResolver(provider: EditionProvider) {
/** Runs the edition resolution.
*
* @param edition the raw edition to resolve
* @return either a resolution error or the resolved edition
*/
def resolve(
edition: RawEdition
): Either[EditionResolutionError, ResolvedEdition] =
resolveEdition(edition, Nil)
/** A helper method that resolves an edition and keeps a list of already
* visited edition names to avoid cycles.
*/
private def resolveEdition(
edition: RawEdition,
visitedEditions: List[String]
): Either[EditionResolutionError, ResolvedEdition] =
for {
parent <- resolveParent(edition.parent, visitedEditions)
libraries <- resolveLibraries(
edition.libraries,
edition.repositories,
parent
)
} yield Editions.Resolved.Edition(
parent = parent,
engineVersion = edition.engineVersion,
repositories = edition.repositories,
libraries = libraries
)
/** A helper method for resolving libraries.
*
* @param libraries the raw mapping of libraries
* @param currentRepositories the mapping of repositories defined in the
* current edition
* @param parent an optional reference to an (already resolved) parent
* edition, which is used if the library references a
* repository that was not defined in `currentRepositories`
* @return either an error indicating a reference to an unknown repository or
* a mapping of resolved libraries
*/
private def resolveLibraries(
libraries: Map[String, Editions.Raw.Library],
currentRepositories: Map[String, Editions.Repository],
parent: Option[ResolvedEdition]
): Either[
LibraryReferencesUndefinedRepository,
Map[String, Editions.Resolved.Library]
] = {
val resolvedPairs: Either[
LibraryReferencesUndefinedRepository,
List[(String, Editions.Resolved.Library)]
] =
libraries.toList.traverse { case (name, library) =>
val resolved = resolveLibrary(library, currentRepositories, parent)
resolved.map((name, _))
}
resolvedPairs.map(Map.from)
}
/** A helper method to resolve a single library.
*
* @param library the library to resolve
* @param currentRepositories the mapping of repositories defined in the
* current edition
* @param parent an optional reference to an (already resolved) parent
* edition, which is used if the library references a
* repository that was not defined in `currentRepositories`
* @return either an error or the resolved library
*/
@tailrec
private def resolveLibrary(
library: Editions.Raw.Library,
currentRepositories: Map[String, Editions.Repository],
parent: Option[ResolvedEdition]
): Either[
LibraryReferencesUndefinedRepository,
Editions.Resolved.Library
] = library match {
case Editions.Raw.LocalLibrary(qualifiedName) =>
Right(Editions.Resolved.LocalLibrary(qualifiedName))
case Editions.Raw.PublishedLibrary(
qualifiedName,
version,
repositoryName
) =>
(currentRepositories.get(repositoryName), parent) match {
case (Some(repository), _) =>
Right(
Editions.Resolved
.PublishedLibrary(qualifiedName, version, repository)
)
case (None, Some(parentEdition)) =>
resolveLibrary(
library,
parentEdition.repositories,
parentEdition.parent
)
case (None, None) =>
Left(
LibraryReferencesUndefinedRepository(
libraryName = library.qualifiedName,
repositoryName = repositoryName
)
)
}
}
/** A helper function that takes an optional parent name and list of already
* seen editions and tries to load the parent (if present), but avoiding
* cycles (returning an error if a cycle was encountered).
*/
private def resolveParent(
parent: Option[String],
visitedEditions: List[String]
): Either[EditionResolutionError, Option[ResolvedEdition]] = parent match {
case Some(parentName) =>
if (visitedEditions.contains(parentName)) {
val cycle = parentName :: visitedEditions
Left(EditionResolutionCycle(cycle.reverse))
} else
for {
rawParent <- provider
.findEditionForName(parentName)
.toEither
.left
.map(EditionResolutionError.CannotLoadEdition(parentName, _))
res <- resolveEdition(rawParent, parentName :: visitedEditions)
} yield Some(res)
case None => Right(None)
}
}

View File

@ -0,0 +1,259 @@
package org.enso.editions
import cats.Show
import io.circe.syntax.EncoderOps
import io.circe._
import nl.gn0s1s.bump.SemVer
import org.enso.editions.Editions.{Raw, Repository}
import org.enso.editions.SemVerJson._
import java.io.FileReader
import java.net.URL
import java.nio.file.Path
import scala.util.{Failure, Try, Using}
/** Gathers methods for decoding and encoding of Raw editions. */
object EditionSerialization {
/** Tries to parse an edition definition from a string in the YAML format. */
def parseYamlString(yamlString: String): Try[Raw.Edition] =
yaml.parser
.parse(yamlString)
.flatMap(_.as[Raw.Edition])
.left
.map(EditionResolutionError.wrapDecodingError)
.toTry
/** Tries to load an edition definition from a YAML file. */
def loadEdition(path: Path): Try[Raw.Edition] =
Using(new FileReader(path.toFile)) { reader =>
yaml.parser
.parse(reader)
.flatMap(_.as[Raw.Edition])
.toTry
}.flatten
.recoverWith { error =>
Failure(
EditionResolutionError.wrapLoadingError(
path.getFileName.toString,
error
)
)
}
/** A [[Decoder]] instance for [[Raw.Edition]].
*
* It can be used to decode nested editions in other kinds of configurations
* files, for example in `package.yaml`.
*/
implicit val editionDecoder: Decoder[Raw.Edition] = { json =>
for {
parent <- json.get[Option[EditionName]](Fields.parent)
engineVersion <- json.get[Option[EnsoVersion]](Fields.engineVersion)
_ <-
if (parent.isEmpty && engineVersion.isEmpty)
Left(
DecodingFailure(
s"The edition must specify at least one of " +
s"${Fields.engineVersion} or ${Fields.parent}.",
json.history
)
)
else Right(())
repositories <-
json.getOrElse[Seq[Repository]](Fields.repositories)(Seq())
libraries <- json.getOrElse[Seq[Raw.Library]](Fields.libraries)(Seq())
res <- {
val repositoryMap = Map.from(repositories.map(r => (r.name, r)))
val libraryMap = Map.from(libraries.map(l => (l.qualifiedName, l)))
if (libraryMap.size != libraries.size)
Left(
DecodingFailure(
"Names of libraries defined within a single edition file must be unique.",
json.downField(Fields.libraries).history
)
)
else if (repositoryMap.size != repositories.size)
Left(
DecodingFailure(
"Names of repositories defined within a single edition file must be unique.",
json.downField(Fields.libraries).history
)
)
else
Right(
Raw.Edition(
parent = parent.map(_.name),
engineVersion = engineVersion,
repositories = repositoryMap,
libraries = libraryMap
)
)
}
} yield res
}
/** An [[Encoder]] instance for [[Raw.Edition]]. */
implicit val editionEncoder: Encoder[Raw.Edition] = { edition =>
val parent = edition.parent.map { parent => Fields.parent -> parent.asJson }
val engineVersion = edition.engineVersion.map { version =>
Fields.engineVersion -> version.asJson
}
if (parent.isEmpty && engineVersion.isEmpty) {
throw new IllegalArgumentException(
"Internal error: An edition must specify at least the engine version or extends clause"
)
}
val repositories =
if (edition.repositories.isEmpty) None
else Some(Fields.repositories -> edition.repositories.values.asJson)
val libraries =
if (edition.libraries.isEmpty) None
else Some(Fields.libraries -> edition.libraries.values.asJson)
Json.obj(
parent.toSeq ++ engineVersion.toSeq ++ repositories.toSeq ++ libraries.toSeq: _*
)
}
object Fields {
val name = "name"
val version = "version"
val repository = "repository"
val url = "url"
val parent = "extends"
val engineVersion = "engine-version"
val repositories = "repositories"
val libraries = "libraries"
val localRepositoryName = "local"
}
case class EditionLoadingError(message: String, cause: Throwable)
extends RuntimeException(message, cause) {
/** @inheritdoc */
override def toString: String = message
}
object EditionLoadingError {
/** Creates a [[EditionLoadingError]] by wrapping another [[Throwable]].
*
* Special logic is used for [[io.circe.Error]] to display the error
* summary in a human-readable way.
*/
def fromThrowable(throwable: Throwable): EditionLoadingError =
throwable match {
case decodingError: io.circe.Error =>
val errorMessage =
implicitly[Show[io.circe.Error]].show(decodingError)
EditionLoadingError(
s"Could not parse the edition file: $errorMessage",
decodingError
)
case other =>
EditionLoadingError(s"Could not load the edition file: $other", other)
}
}
/** A helper opaque type to handle special parsing logic of edition names.
*
* The issue is that if an edition is called `2021.4` and it is written
* unquoted inside of a YAML file, that is treated as a floating point
* number, so special care must be taken to correctly parse it.
*/
private case class EditionName(name: String)
implicit private val editionNameDecoder: Decoder[EditionName] = { json =>
json
.as[String]
.orElse(json.as[Int].map(_.toString))
.orElse(json.as[Float].map(_.toString))
.map(EditionName)
}
implicit private val libraryDecoder: Decoder[Raw.Library] = { json =>
def makeLibrary(name: String, repository: String, version: Option[SemVer]) =
if (repository == Fields.localRepositoryName)
if (version.isDefined)
Left(
DecodingFailure(
"Version field must not be set for libraries associated with the local repository.",
json.history
)
)
else Right(Raw.LocalLibrary(name))
else {
version match {
case Some(versionValue) =>
Right(Raw.PublishedLibrary(name, versionValue, repository))
case None =>
Left(
DecodingFailure(
"Version field is mandatory for non-local libraries.",
json.history
)
)
}
}
for {
name <- json.get[String](Fields.name)
repository <- json.get[String](Fields.repository)
version <- json.get[Option[SemVer]](Fields.version)
res <- makeLibrary(name, repository, version)
} yield res
}
implicit private val libraryEncoder: Encoder[Raw.Library] = {
case Raw.LocalLibrary(name) =>
Json.obj(
Fields.name -> name.asJson,
Fields.repository -> Fields.localRepositoryName.asJson
)
case Raw.PublishedLibrary(name, version, repository) =>
Json.obj(
Fields.name -> name.asJson,
Fields.version -> version.asJson,
Fields.repository -> repository.asJson
)
}
implicit private val urlEncoder: Encoder[URL] = { url => url.toString.asJson }
implicit private val urlDecoder: Decoder[URL] = { json =>
json.as[String].flatMap { str =>
Try(new URL(str)).toEither.left.map(throwable =>
DecodingFailure(
s"Cannot parse an URL: ${throwable.getMessage}",
json.history
)
)
}
}
implicit private val repositoryEncoder: Encoder[Repository] = { repo =>
Json.obj(
Fields.name -> repo.name.asJson,
Fields.url -> repo.url.asJson
)
}
implicit private val repositoryDecoder: Decoder[Repository] = { json =>
val nameField = json.downField(Fields.name)
for {
name <- nameField.as[String]
url <- json.get[URL](Fields.url)
res <-
if (name == Fields.localRepositoryName)
Left(
DecodingFailure(
s"A defined repository cannot be called " +
s"`${Fields.localRepositoryName}` which is a reserved keyword.",
nameField.history
)
)
else Right(Repository(name, url))
} yield res
}
}

View File

@ -0,0 +1,147 @@
package org.enso.editions
import nl.gn0s1s.bump.SemVer
import java.net.URL
import scala.util.Try
/** Defines the general edition structure.
*
* We split the data type into two categories: Raw and Resolved editions.
*
* Raw editions are directly parsed from a YAML structure and can be serialized
* back to it. They are just an object model of the underlying YAML format.
*
* The raw edition may reference a parent edition or repositories by name.
* The [[EditionResolver]] takes care of resolving a Raw edition into a
* Resolved instance by loading and parsing any of its parents and replacing
* the repository by-name references by actual references to the repository
* instances.
*/
trait Editions {
/** The type of nested editions.
*
* Raw editions will refer to parents by their name and the Resolved edition
* will contain a reference to an actual object.
*/
type NestedEditionType
/** The type of repository reference in listed libraries.
*
* Libraries in Raw editions will refer to repositories by their name and the
* Resolved variant will contain a reference to the actual repository object
* loaded from the edition or one of its parents.
*/
type LibraryRepositoryType
/** The library description included in the edition. */
sealed trait Library {
/** The qualified name of the library.
*
* It should consist of a prefix followed by a dot an the library name, for
* example `Prefix.Library_Name`.
*/
def qualifiedName: String
}
/** Represents a local library. */
case class LocalLibrary(override val qualifiedName: String) extends Library
/** Represents a specific version of the library that is published in a
* repository.
*
* @param qualifiedName the qualified name of the library
* @param version the exact version of the library that should be used
* @param repository the recommended repository to download the library from,
* if it is not yet cached
*/
case class PublishedLibrary(
override val qualifiedName: String,
version: SemVer,
repository: LibraryRepositoryType
) extends Library
/** An Edition describing the library resolution configuration.
*
* @param parent a parent edition (if applicable)
* @param engineVersion an engine version; it should be defined if the
* edition wants to override the setting from the parent
* or if it has no parents
* @param repositories a mapping of repositories directly defined in the
* edition (does not include ones defined in the parents)
* @param libraries a mapping of libraries directly defined in the edition
* (does not include ones defined in the parents)
*/
case class Edition(
parent: Option[NestedEditionType],
engineVersion: Option[EnsoVersion],
repositories: Map[String, Editions.Repository],
libraries: Map[String, Library]
) {
if (parent.isEmpty && engineVersion.isEmpty)
throw new IllegalArgumentException(
"The edition must specify the engine version or a parent edition " +
"that will imply it."
)
}
}
object Editions {
/** Represents a repository that provides libraries. */
case class Repository(name: String, url: URL)
object Repository {
/** A helper function that creates a Repository instance from a raw string
* URL.
*/
def make(name: String, url: String): Try[Repository] = Try {
Repository(name, new URL(url))
}
}
/** Implements the Raw editions that can be directly parsed from a YAML
* configuration.
*
* All references are typed as String, because this is what is directly read
* from the configuration, without any postprocessing.
*/
object Raw extends Editions {
override type NestedEditionType = String
override type LibraryRepositoryType = String
}
/** Implements the Resolved editions which are obtained by analyzing the Raw
* edition and loading any of its parents.
*/
object Resolved extends Editions {
override type NestedEditionType = this.Edition
override type LibraryRepositoryType = Repository
}
/** An alias for Raw editions. */
type RawEdition = Raw.Edition
/** An alias for Resolved editions. */
type ResolvedEdition = Resolved.Edition
/** Syntax helpers for resolved edition. */
implicit class ResolvedEditionOps(edition: ResolvedEdition) {
/** Resolves the engine version that is implied by this resolved edition. It
* is either the version override directly specified in the edition or the
* version implied by its parent.
*/
def getEngineVersion: EnsoVersion = edition.engineVersion.getOrElse {
val parent = edition.parent.getOrElse {
throw new IllegalStateException(
"Internal error: Resolved edition does not imply an engine version."
)
}
parent.getEngineVersion
}
}
}

View File

@ -0,0 +1,27 @@
package org.enso.editions
import org.enso.editions.Editions.RawEdition
import org.enso.editions.provider.EditionProvider
/** A helper class which resolves the engine version that is entailed by the
* edition configuration.
*
* It requires an [[EditionProvider]] instance, because the edition may not
* specify the engine version directly, but it can just rely on what is
* specified in a parent edition, so it needs a way to load the parent
* editions.
*/
case class EngineVersionResolver(editionProvider: EditionProvider) {
private val editionResolver = EditionResolver(editionProvider)
/** Returns the [[EnsoVersion]] that is entailed by the provided edition (or a
* resolution error if the edition cannot be resolved).
*/
def resolveEnsoVersion(
edition: RawEdition
): Either[EditionResolutionError, EnsoVersion] = {
for {
edition <- editionResolver.resolve(edition)
} yield edition.getEngineVersion
}
}

View File

@ -1,4 +1,4 @@
package org.enso.pkg
package org.enso.editions
import io.circe.syntax._
import io.circe.{Decoder, DecodingFailure, Encoder}

View File

@ -0,0 +1,16 @@
package org.enso.editions
/** Represents a library name that should uniquely identify the library.
*
* The prefix is either a special prefix or a username.
*/
case class LibraryName(prefix: String, name: String) {
/** The qualified name of the library consists of its prefix and name
* separated with a dot.
*/
def qualifiedName: String = s"$prefix.$name"
/** @inheritdoc */
override def toString: String = qualifiedName
}

View File

@ -1,7 +1,7 @@
package org.enso.pkg
package org.enso.editions
import io.circe.{Decoder, DecodingFailure, Encoder}
import io.circe.syntax._
import io.circe.{Decoder, DecodingFailure, Encoder}
import nl.gn0s1s.bump.SemVer
object SemVerJson {

View File

@ -0,0 +1,16 @@
package org.enso.editions.provider
import org.enso.editions.Editions
import scala.util.Try
/** Interface for a provider of editions which is able to load a raw edition
* based on its name.
*
* It is used when resolving parent edition references.
*/
trait EditionProvider {
/** Tries to load an edition with the given name. */
def findEditionForName(name: String): Try[Editions.Raw.Edition]
}

View File

@ -0,0 +1,63 @@
package org.enso.editions.provider
import org.enso.editions.{EditionSerialization, Editions}
import java.io.FileNotFoundException
import java.nio.file.{Files, Path}
import scala.annotation.tailrec
import scala.util.{Failure, Success, Try}
/** An implementation of [[EditionProvider]] that looks for the edition files in
* a list of filesystem paths.
*/
case class FileSystemEditionProvider(searchPaths: List[Path])
extends EditionProvider {
/** @inheritdoc */
override def findEditionForName(name: String): Try[Editions.Raw.Edition] = {
val result = findEdition(name, searchPaths)
result match {
case Left(EditionNotFound) =>
Failure(new FileNotFoundException(s"Could not find edition `$name`."))
case Left(EditionReadError(error)) => Failure(error)
case Right(value) => Success(value)
}
}
private val editionSuffix = ".yaml"
@tailrec
private def findEdition(
name: String,
paths: List[Path]
): Either[EditionLoadingError, Editions.Raw.Edition] = paths match {
case head :: tail =>
val headResult = loadEdition(name, head)
headResult match {
case Left(EditionNotFound) =>
findEdition(name, tail)
case _ => headResult
}
case Nil => Left(EditionNotFound)
}
sealed private trait EditionLoadingError
private case object EditionNotFound extends EditionLoadingError
private case class EditionReadError(error: Throwable)
extends EditionLoadingError
private def loadEdition(
name: String,
path: Path
): Either[EditionLoadingError, Editions.Raw.Edition] = {
val fileName = name + editionSuffix
val editionPath = path.resolve(fileName)
if (Files.exists(editionPath)) {
EditionSerialization
.loadEdition(editionPath)
.toEither
.left
.map(EditionReadError)
} else Left(EditionNotFound)
}
}

View File

@ -0,0 +1,157 @@
package org.enso.editions
import nl.gn0s1s.bump.SemVer
import org.enso.editions.EditionResolutionError.EditionResolutionCycle
import org.enso.editions.Editions.Repository
import org.enso.editions.provider.EditionProvider
import org.scalatest.matchers.should.Matchers
import org.scalatest.wordspec.AnyWordSpec
import org.scalatest.{Inside, OptionValues}
import scala.util.{Failure, Success, Try}
class EditionResolverSpec
extends AnyWordSpec
with Matchers
with Inside
with OptionValues {
object FakeEditionProvider extends EditionProvider {
val mainRepo = Repository.make("main", "https://example.com/main").get
val editions: Map[String, Editions.RawEdition] = Map(
"2021.0" -> Editions.Raw.Edition(
parent = None,
engineVersion = Some(SemVerEnsoVersion(SemVer(1, 2, 3))),
repositories = Map(
"main" -> mainRepo
),
libraries = Map(
"Standard.Base" -> Editions.Raw
.PublishedLibrary("Standard.Base", SemVer(1, 2, 3), "main")
)
),
"cycleA" -> Editions.Raw.Edition(
parent = Some("cycleB"),
engineVersion = Some(SemVerEnsoVersion(SemVer(1, 2, 3))),
repositories = Map(),
libraries = Map()
),
"cycleB" -> Editions.Raw.Edition(
parent = Some("cycleA"),
engineVersion = Some(SemVerEnsoVersion(SemVer(1, 2, 3))),
repositories = Map(),
libraries = Map()
)
)
override def findEditionForName(name: String): Try[Editions.Raw.Edition] =
editions
.get(name)
.map(Success(_))
.getOrElse(
Failure(new RuntimeException(s"Could not load edition `$name`."))
)
}
val resolver = EditionResolver(FakeEditionProvider)
"EditionResolver" should {
"resolve a simple edition" in {
val repo = Repository.make("foo", "http://example.com").get
val edition = Editions.Raw.Edition(
parent = None,
engineVersion = Some(SemVerEnsoVersion(SemVer(1, 2, 3))),
repositories = Map("foo" -> repo),
libraries = Map(
"bar.baz" -> Editions.Raw.LocalLibrary("bar.baz"),
"foo.bar" -> Editions.Raw
.PublishedLibrary("foo.bar", SemVer(1, 2, 3), "foo")
)
)
inside(resolver.resolve(edition)) { case Right(resolved) =>
resolved.engineVersion shouldEqual edition.engineVersion
resolved.parent should be(empty)
resolved.repositories shouldEqual edition.repositories
resolved.libraries should have size 2
resolved.libraries("bar.baz") shouldEqual Editions.Resolved
.LocalLibrary("bar.baz")
resolved.libraries("foo.bar") shouldEqual Editions.Resolved
.PublishedLibrary("foo.bar", SemVer(1, 2, 3), repo)
}
}
"resolve a nested edition" in {
val edition = Editions.Raw.Edition(
parent = Some("2021.0"),
engineVersion = Some(SemVerEnsoVersion(SemVer(1, 2, 3))),
repositories = Map(),
libraries = Map(
"bar.baz" -> Editions.Raw.LocalLibrary("bar.baz"),
"foo.bar" -> Editions.Raw
.PublishedLibrary("foo.bar", SemVer(1, 2, 3), "main")
)
)
inside(resolver.resolve(edition)) { case Right(resolved) =>
resolved.parent should be(defined)
resolved.libraries should have size 2
resolved.libraries("bar.baz") shouldEqual Editions.Resolved
.LocalLibrary("bar.baz")
resolved.libraries("foo.bar") shouldEqual Editions.Resolved
.PublishedLibrary(
"foo.bar",
SemVer(1, 2, 3),
FakeEditionProvider.mainRepo
)
}
}
"correctly handle repository shadowing when resolving libraries" in {
val localRepo = Repository.make("main", "http://example.com/local").get
localRepo should not equal FakeEditionProvider.mainRepo
val edition = Editions.Raw.Edition(
parent = Some("2021.0"),
engineVersion = None,
repositories = Map("main" -> localRepo),
libraries = Map(
"foo.bar" -> Editions.Raw
.PublishedLibrary("foo.bar", SemVer(1, 2, 3), "main")
)
)
inside(resolver.resolve(edition)) { case Right(resolved) =>
resolved.parent should be(defined)
resolved.libraries should have size 1
resolved.libraries("foo.bar") shouldEqual
Editions.Resolved.PublishedLibrary(
"foo.bar",
SemVer(1, 2, 3),
localRepo
)
resolved.parent.value.libraries("Standard.Base") shouldEqual
Editions.Resolved.PublishedLibrary(
"Standard.Base",
SemVer(1, 2, 3),
FakeEditionProvider.mainRepo
)
}
}
"avoid cycles in the resolution" in {
val edition = Editions.Raw.Edition(
parent = Some("cycleA"),
engineVersion = Some(SemVerEnsoVersion(SemVer(1, 2, 3))),
repositories = Map(),
libraries = Map()
)
val failure = resolver.resolve(edition)
inside(failure) { case Left(EditionResolutionCycle(editions)) =>
editions shouldEqual Seq("cycleA", "cycleB", "cycleA")
}
}
}
}

View File

@ -0,0 +1,121 @@
package org.enso.editions
import nl.gn0s1s.bump.SemVer
import org.scalatest.Inside
import org.scalatest.matchers.should.Matchers
import org.scalatest.wordspec.AnyWordSpec
import scala.util.{Failure, Success}
class EditionSerializationSpec extends AnyWordSpec with Matchers with Inside {
"EditionSerialization" should {
"parse an edition that just derives another one" in {
val parsed = EditionSerialization.parseYamlString(
"""extends: 2021.4
|""".stripMargin
)
inside(parsed) { case Success(edition) =>
edition.parent should contain("2021.4")
edition.repositories should be(empty)
edition.libraries should be(empty)
edition.engineVersion should be(empty)
}
}
"parse a standalone edition" in {
val parsed = EditionSerialization.parseYamlString(
"""engine-version: 1.2.3-SNAPSHOT
|repositories:
| - name: example
| url: https://example.com/
| - name: foo
| url: http://127.0.0.1:8080/root
|libraries:
| - name: Foo.Local
| repository: local
| - name: Bar.Baz
| repository: example
| version: 0.0.0
| - name: A.B
| repository: bar
| version: 1.0.1
|""".stripMargin
)
inside(parsed) { case Success(edition) =>
edition.parent should be(empty)
edition.repositories.size shouldEqual 2
edition.repositories("example").name shouldEqual "example"
edition
.repositories("example")
.url
.toString shouldEqual "https://example.com/"
edition.repositories("foo").name shouldEqual "foo"
edition
.repositories("foo")
.url
.toString shouldEqual "http://127.0.0.1:8080/root"
edition.libraries.values should contain theSameElementsAs Seq(
Editions.Raw.LocalLibrary("Foo.Local"),
Editions.Raw.PublishedLibrary("Bar.Baz", SemVer(0, 0, 0), "example"),
Editions.Raw.PublishedLibrary("A.B", SemVer(1, 0, 1), "bar")
)
edition.engineVersion should contain(
SemVerEnsoVersion(SemVer(1, 2, 3, Some("SNAPSHOT")))
)
}
}
"not parse an edition that does not define the engine version nor attempt to derive it" in {
val parsed = EditionSerialization.parseYamlString(
"""libraries:
|- name: foo
| repository: local
|""".stripMargin
)
inside(parsed) { case Failure(exception) =>
exception.getMessage should include(
"The edition must specify at least one of"
)
}
}
"not allow invalid urls for repositories" in {
val parsed = EditionSerialization.parseYamlString(
"""extends: foo
|repositories:
|- name: bar
| url: baz
|""".stripMargin
)
inside(parsed) { case Failure(exception) =>
exception.getMessage should include("Cannot parse an URL")
}
}
"not allow invalid version combinations for libraries" in {
val parsed = EditionSerialization.parseYamlString(
"""extends: foo
|libraries:
|- name: bar
| repository: local
| version: 1.2.3-SHOULD-NOT-BE-HERE
|""".stripMargin
)
inside(parsed) { case Failure(exception) =>
exception.getMessage should include("Version field must not be set")
}
val parsed2 = EditionSerialization.parseYamlString(
"""extends: foo
|libraries:
|- name: bar
| repository: something
|""".stripMargin
)
inside(parsed2) { case Failure(exception) =>
exception.getMessage should include("Version field is mandatory")
}
}
}
}

View File

@ -0,0 +1,61 @@
package org.enso.librarymanager
import org.enso.distribution.DistributionManager
import org.enso.editions.{Editions, LibraryName}
import org.enso.librarymanager.local.LocalLibraryProvider
import org.enso.librarymanager.published.{
DefaultPublishedLibraryProvider,
PublishedLibraryProvider
}
/** A helper class for loading libraries. */
case class LibraryLoader(distributionManager: DistributionManager) {
private val localLibraryProvider =
LocalLibraryProvider.make(distributionManager)
private val resolver = LibraryResolver(localLibraryProvider)
private val publishedLibraryProvider: PublishedLibraryProvider =
new DefaultPublishedLibraryProvider(distributionManager)
/** Resolves the library version that should be used based on the
* configuration and returns its location on the filesystem.
*
* If the library is not available, this operation may download it.
*/
def findLibrary(
libraryName: LibraryName,
edition: Editions.ResolvedEdition,
preferLocalLibraries: Boolean
): LibraryResolutionResult = {
val resolvedVersion = resolver
.resolveLibraryVersion(libraryName, edition, preferLocalLibraries)
resolvedVersion match {
case Left(error) =>
LibraryResolutionResult.ResolutionFailure(error)
case Right(LibraryVersion.Local) =>
localLibraryProvider
.findLibrary(libraryName)
.map(LibraryResolutionResult.ResolvedImmediately)
.getOrElse {
LibraryResolutionResult.ResolutionFailure(
LibraryResolutionError(
s"Edition configuration forces to use the local version, but " +
s"the `$libraryName` library is not present among local " +
s"libraries."
)
)
}
case Right(LibraryVersion.Published(version, repository)) =>
val dependencyResolver = { name: LibraryName =>
resolver
.resolveLibraryVersion(name, edition, preferLocalLibraries)
.toOption
}
publishedLibraryProvider.findLibrary(
libraryName,
version,
repository,
dependencyResolver
)
}
}
}

View File

@ -0,0 +1,5 @@
package org.enso.librarymanager
/** Indicates an error of library resolution. */
case class LibraryResolutionError(message: String)
extends RuntimeException(message)

View File

@ -0,0 +1,32 @@
package org.enso.librarymanager
import org.enso.cli.task.TaskProgress
import java.nio.file.Path
/** Encapsulates possible results of library resolution. */
sealed trait LibraryResolutionResult
object LibraryResolutionResult {
/** Indicates that the resolution has failed immediately with an error. */
case class ResolutionFailure(error: Throwable) extends LibraryResolutionResult
/** Indicates that the resolution has succeeded immediately.
*
* The library was already available on disk and the path to it is returned.
*/
case class ResolvedImmediately(path: Path) extends LibraryResolutionResult
/** Indicates that the initial library resolution has succeeded (that the
* corrrect version could be inferred from the edition) but the library was
* not installed, so it must be downloaded.
*
* It contains a [[TaskProgress]] instance that can track the progress of the
* download and will be completed once the library has been installed.
*
* The download and installation may of course fail as well, which is
* indicated by failure of the [[TaskProgress]].
*/
case class ResolutionPending(result: TaskProgress[Path])
extends LibraryResolutionResult
}

View File

@ -0,0 +1,78 @@
package org.enso.librarymanager
import org.enso.editions.{Editions, LibraryName}
import org.enso.librarymanager.local.LocalLibraryProvider
/** A helper class that figures out which library version should be used in a
* given configuration.
*
* It needs a [[LocalLibraryProvider]] instance, because if
* `preferLocalLibraries` is set to true, the resulting library version depends
* on whether a local library with a given name exists.
*/
case class LibraryResolver(
localLibraryProvider: LocalLibraryProvider
) {
/** Resolves the library version that entails from the provided configuration.
*
* The configuration consists of the edition and the flag specifying if local
* libraries should override the edition settings.
*
* @param libraryName the name of the library to resolve
* @param edition the edition configuration
* @param preferLocalLibraries the flag indicating whether to prefer local
* libraries that are present over what the
* edition defines
* @return
*/
def resolveLibraryVersion(
libraryName: LibraryName,
edition: Editions.ResolvedEdition,
preferLocalLibraries: Boolean
): Either[LibraryResolutionError, LibraryVersion] = {
if (preferLocalLibraries) {
localLibraryProvider
.findLibrary(libraryName)
.map(_ => Right(LibraryVersion.Local))
.getOrElse {
resolveLibraryFromEdition(libraryName, edition)
}
} else resolveLibraryFromEdition(libraryName, edition)
}
/** Resolves the library version that entails from the edition.
*
* @param libraryName the name of the library to resolve
* @param edition the edition configuration
* @return either an error (if the library was not found in the edition) or
* the entailed version
*/
def resolveLibraryFromEdition(
libraryName: LibraryName,
edition: Editions.ResolvedEdition
): Either[LibraryResolutionError, LibraryVersion] = {
import Editions.Resolved._
val immediateResult =
edition.libraries.get(libraryName.qualifiedName).map {
case LocalLibrary(_) =>
Right(LibraryVersion.Local)
case PublishedLibrary(_, version, repository) =>
Right(LibraryVersion.Published(version, repository))
}
immediateResult.getOrElse {
edition.parent match {
case Some(parentEdition) =>
resolveLibraryFromEdition(libraryName, parentEdition)
case None =>
Left(
LibraryResolutionError(
s"The library `$libraryName` is not defined within " +
s"the edition."
)
)
}
}
}
}

View File

@ -0,0 +1,20 @@
package org.enso.librarymanager
import nl.gn0s1s.bump.SemVer
import org.enso.editions.Editions.Repository
/** A resolved version of the library. */
sealed trait LibraryVersion
object LibraryVersion {
/** Indicates that the version from the local library path should be used. */
case object Local extends LibraryVersion
/** Indicates that a particular published version should be used.
*
* It also indicates the repository to download the library from if it is not
* already cached.
*/
case class Published(version: SemVer, repository: Repository)
extends LibraryVersion
}

View File

@ -0,0 +1,37 @@
package org.enso.librarymanager.local
import org.enso.distribution.DistributionManager
import org.enso.editions.LibraryName
import org.enso.distribution.FileSystem.PathSyntax
import java.nio.file.{Files, Path}
import scala.annotation.tailrec
/** A default implementation of [[LocalLibraryProvider]]. */
class DefaultLocalLibraryProvider(distributionManager: DistributionManager)
extends LocalLibraryProvider {
/** @inheritdoc */
override def findLibrary(libraryName: LibraryName): Option[Path] =
findLibraryHelper(
libraryName,
distributionManager.paths.localLibrariesSearchPaths.toList
)
/** Searches through the available library paths, checking if any one of them contains the requested library.
*
* The first path on the list takes precedence.
*/
@tailrec
private def findLibraryHelper(
libraryName: LibraryName,
searchPaths: List[Path]
): Option[Path] = searchPaths match {
case head :: tail =>
val potentialPath = head / libraryName.prefix / libraryName.name
if (Files.exists(potentialPath) && Files.isDirectory(potentialPath))
Some(potentialPath)
else findLibraryHelper(libraryName, tail)
case Nil => None
}
}

View File

@ -0,0 +1,23 @@
package org.enso.librarymanager.local
import org.enso.distribution.DistributionManager
import org.enso.editions.LibraryName
import java.nio.file.Path
/** A provider for local libraries. */
trait LocalLibraryProvider {
/** Returns the path to a local instance of the requested library, if it is
* available.
*/
def findLibrary(libraryName: LibraryName): Option[Path]
}
object LocalLibraryProvider {
/** Creates a default [[LocalLibraryProvider]] from a [[DistributionManager]].
*/
def make(distributionManager: DistributionManager): LocalLibraryProvider =
new DefaultLocalLibraryProvider(distributionManager)
}

View File

@ -0,0 +1,28 @@
package org.enso.librarymanager.published
import nl.gn0s1s.bump.SemVer
import org.enso.distribution.DistributionManager
import org.enso.editions.{Editions, LibraryName}
import org.enso.librarymanager.{LibraryResolutionResult, LibraryVersion}
import scala.annotation.nowarn
/** A default implementation of [[PublishedLibraryProvider]]. */
class DefaultPublishedLibraryProvider(
@nowarn("msg=never used") distributionManager: DistributionManager
) extends PublishedLibraryProvider {
// TODO [RW] This is just a stub and will be properly implemented in #1772.
/** @inheritdoc */
override def findLibrary(
libraryName: LibraryName,
version: SemVer,
recommendedRepository: Editions.Repository,
dependencyResolver: LibraryName => Option[LibraryVersion]
): LibraryResolutionResult = LibraryResolutionResult.ResolutionFailure(
new NotImplementedError(
"TODO library management is not yet implemented"
)
)
}

View File

@ -0,0 +1,27 @@
package org.enso.librarymanager.published
import nl.gn0s1s.bump.SemVer
import org.enso.editions.Editions.Repository
import org.enso.editions.LibraryName
import org.enso.librarymanager.{LibraryResolutionResult, LibraryVersion}
/** A provider of published libraries.
*
* It usually should use some kind of a cache to keep already downloaded
* libraries and download new libraries on demand.
*/
trait PublishedLibraryProvider {
/** Tries to locate the requested library at a specific version.
*
* If the library is not present, a download may be attempted - this is where
* the `recommendedRepository` is used to guide which repository should be
* used for the download.
*/
def findLibrary(
libraryName: LibraryName,
version: SemVer,
recommendedRepository: Repository,
dependencyResolver: LibraryName => Option[LibraryVersion]
): LibraryResolutionResult
}

View File

@ -0,0 +1,146 @@
package org.enso.librarymanager
import nl.gn0s1s.bump.SemVer
import org.enso.editions.Editions.Repository
import org.enso.editions.{DefaultEnsoVersion, Editions, LibraryName}
import org.enso.librarymanager.local.LocalLibraryProvider
import org.enso.testkit.EitherValue
import org.scalatest.Inside
import org.scalatest.matchers.should.Matchers
import org.scalatest.wordspec.AnyWordSpec
import java.nio.file.Path
class LibraryResolverSpec
extends AnyWordSpec
with Matchers
with EitherValue
with Inside {
"LibraryResolver" should {
val mainRepo = Repository.make("main", "https://example.com/main").get
val parentEdition = Editions.Resolved.Edition(
parent = None,
engineVersion = Some(DefaultEnsoVersion),
repositories = Map("main" -> mainRepo),
libraries = Map(
"Standard.Base" -> Editions.Resolved
.PublishedLibrary("Standard.Base", SemVer(4, 5, 6), mainRepo)
)
)
val customRepo = Repository.make("custom", "https://example.com/custom").get
val currentEdition = Editions.Resolved.Edition(
parent = Some(parentEdition),
engineVersion = None,
repositories = Map("custom" -> customRepo),
libraries = Map(
"Foo.Main" -> Editions.Resolved
.PublishedLibrary("Foo.Main", SemVer(1, 0, 0), mainRepo),
"Foo.My" -> Editions.Resolved
.PublishedLibrary("Foo.My", SemVer(2, 0, 0), customRepo),
"Foo.Local" -> Editions.Resolved.LocalLibrary("Foo.Local")
)
)
case class FakeLocalLibraryProvider(fixtures: Map[String, Path])
extends LocalLibraryProvider {
override def findLibrary(libraryName: LibraryName): Option[Path] =
fixtures.get(libraryName.qualifiedName)
}
val localLibraries = Map(
"Foo.My" -> Path.of("./Foo/My"),
"Foo.Local" -> Path.of("./Foo/Local"),
"Standard.Base" -> Path.of("./Standard/Base")
)
val resolver = LibraryResolver(FakeLocalLibraryProvider(localLibraries))
"correctly resolve libraries based on the edition" in {
resolver
.resolveLibraryVersion(
libraryName = LibraryName("Standard", "Base"),
edition = currentEdition,
preferLocalLibraries = false
)
.rightValue shouldEqual
LibraryVersion.Published(SemVer(4, 5, 6), mainRepo)
resolver
.resolveLibraryVersion(
libraryName = LibraryName("Foo", "Main"),
edition = currentEdition,
preferLocalLibraries = false
)
.rightValue shouldEqual
LibraryVersion.Published(SemVer(1, 0, 0), mainRepo)
resolver
.resolveLibraryVersion(
libraryName = LibraryName("Foo", "My"),
edition = currentEdition,
preferLocalLibraries = false
)
.rightValue shouldEqual
LibraryVersion.Published(SemVer(2, 0, 0), customRepo)
resolver
.resolveLibraryVersion(
libraryName = LibraryName("Foo", "Local"),
edition = currentEdition,
preferLocalLibraries = false
)
.rightValue shouldEqual
LibraryVersion.Local
}
"correctly handle preference of local libraries" in {
resolver
.resolveLibraryVersion(
libraryName = LibraryName("Standard", "Base"),
edition = currentEdition,
preferLocalLibraries = true
)
.rightValue shouldEqual
LibraryVersion.Local
resolver
.resolveLibraryVersion(
libraryName = LibraryName("Foo", "Main"),
edition = currentEdition,
preferLocalLibraries = true
)
.rightValue shouldEqual
LibraryVersion.Published(SemVer(1, 0, 0), mainRepo)
resolver
.resolveLibraryVersion(
libraryName = LibraryName("Foo", "My"),
edition = currentEdition,
preferLocalLibraries = true
)
.rightValue shouldEqual
LibraryVersion.Local
resolver
.resolveLibraryVersion(
libraryName = LibraryName("Foo", "Local"),
edition = currentEdition,
preferLocalLibraries = true
)
.rightValue shouldEqual
LibraryVersion.Local
}
"not fall back to a local library if it was not defined as such " +
"explicitly nor `prefer-local-libraries` is set" in {
val result = resolver.resolveLibraryVersion(
libraryName = LibraryName("Foo", "Local"),
edition = parentEdition,
preferLocalLibraries = false
)
inside(result) { case Left(error) =>
error.getMessage should include("not defined")
}
}
}
}

View File

@ -1,18 +1,13 @@
package org.enso.pkg
import io.circe.syntax._
import io.circe.generic.auto._
import io.circe.{yaml, Decoder, DecodingFailure, Encoder, Json}
import io.circe.{yaml, Decoder, DecodingFailure, Encoder, Json, JsonObject}
import io.circe.yaml.Printer
import org.enso.editions.{DefaultEnsoVersion, Editions, EnsoVersion}
import org.enso.editions.EditionSerialization._
import scala.util.Try
/** An extra project dependency.
* @param name name of the package
* @param version package version
*/
case class Dependency(name: String, version: String)
/** Contact information to a user.
*
* Used for defining authors and maintainers.
@ -26,8 +21,7 @@ case class Contact(name: Option[String], email: Option[String]) {
"At least one of fields `name` or `email` must be defined."
)
/** @inheritdoc
*/
/** @inheritdoc */
override def toString: String = {
val space = if (name.isDefined && email.isDefined) " " else ""
name.getOrElse("") + space + email.map(email => s"<$email>").getOrElse("")
@ -35,15 +29,13 @@ case class Contact(name: Option[String], email: Option[String]) {
}
object Contact {
/** Fields for use when serializing the [[Contact]].
*/
/** Fields for use when serializing the [[Contact]]. */
object Fields {
val Name = "name"
val Email = "email"
}
/** [[Encoder]] instance for the [[Contact]].
*/
/** [[Encoder]] instance for the [[Contact]]. */
implicit val encoder: Encoder[Contact] = { contact =>
val name = contact.name.map(Fields.Name -> _.asJson)
val email = contact.email.map(Fields.Email -> _.asJson)
@ -78,14 +70,15 @@ object Contact {
*
* @param name package name
* @param version package version
* @param ensoVersion version of the Enso engine associated with the package,
* can be set to `default` which defaults to the locally
* installed version
* @param license package license
* @param authors name and contact information of the package author(s)
* @param maintainers name and contact information of current package
* maintainer(s)
* @param dependencies a list of package dependencies
* @param edition the Edition associated with the project; it implies the
* engine version and dependency configuration to be used
* @param preferLocalLibraries specifies if library resolution should prefer
* local libraries over what is defined in the
* edition
* @param originalJson a Json object holding the original values that this
* Config was created from, used to preserve configuration
* keys that are not known
@ -93,16 +86,15 @@ object Contact {
case class Config(
name: String,
version: String,
ensoVersion: EnsoVersion,
license: String,
authors: List[Contact],
maintainers: List[Contact],
dependencies: List[Dependency],
originalJson: Json = Json.obj()
edition: Editions.RawEdition,
preferLocalLibraries: Boolean,
originalJson: JsonObject = JsonObject()
) {
/** Converts the configuration into a YAML representation.
*/
/** Converts the configuration into a YAML representation. */
def toYaml: String =
Printer.spaces2.copy(preserveOrder = true).pretty(Config.encoder(this))
}
@ -115,56 +107,107 @@ object Config {
val license: String = "license"
val author: String = "authors"
val maintainer: String = "maintainers"
val dependencies: String = "dependencies"
val edition: String = "edition"
val preferLocalLibraries = "prefer-local-libraries"
}
implicit val decoder: Decoder[Config] = { json =>
for {
name <- json.get[String](JsonFields.name)
version <- json.getOrElse[String](JsonFields.version)("dev")
ensoVersion <-
json.getOrElse[EnsoVersion](JsonFields.ensoVersion)(DefaultEnsoVersion)
license <- json.getOrElse(JsonFields.license)("")
author <- json.getOrElse[List[Contact]](JsonFields.author)(List())
maintainer <- json.getOrElse[List[Contact]](JsonFields.maintainer)(List())
dependencies <- json.getOrElse[List[Dependency]](JsonFields.dependencies)(
List()
)
name <- json.get[String](JsonFields.name)
version <- json.getOrElse[String](JsonFields.version)("dev")
ensoVersion <- json.get[Option[EnsoVersion]](JsonFields.ensoVersion)
edition <- json.get[Option[Editions.RawEdition]](JsonFields.edition)
license <- json.getOrElse(JsonFields.license)("")
author <- json.getOrElse[List[Contact]](JsonFields.author)(List())
maintainer <- json.getOrElse[List[Contact]](JsonFields.maintainer)(List())
preferLocal <-
json.getOrElse[Boolean](JsonFields.preferLocalLibraries)(false)
finalEdition <-
editionOrVersionBackwardsCompatibility(edition, ensoVersion).left.map {
error => DecodingFailure(error, json.history)
}
originals <- json.as[JsonObject]
} yield Config(
name,
version,
ensoVersion,
license,
author,
maintainer,
dependencies,
json.value
name = name,
version = version,
license = license,
authors = author,
maintainers = maintainer,
edition = finalEdition,
preferLocalLibraries = preferLocal,
originalJson = originals
)
}
implicit val encoder: Encoder[Config] = { config =>
val originals = config.originalJson
val overrides = Json.obj(
JsonFields.name -> config.name.asJson,
JsonFields.version -> config.version.asJson,
JsonFields.ensoVersion -> config.ensoVersion.asJson,
JsonFields.license -> config.license.asJson,
JsonFields.author -> config.authors.asJson,
JsonFields.maintainer -> config.maintainers.asJson
val overrides = Seq(
JsonFields.name -> config.name.asJson,
JsonFields.version -> config.version.asJson,
JsonFields.edition -> config.edition.asJson,
JsonFields.license -> config.license.asJson,
JsonFields.author -> config.authors.asJson,
JsonFields.maintainer -> config.maintainers.asJson
)
val base = originals.deepMerge(overrides)
val withDeps =
if (config.dependencies.nonEmpty)
base.deepMerge(
Json.obj(JsonFields.dependencies -> config.dependencies.asJson)
)
else base
withDeps
val preferLocalOverride =
if (config.preferLocalLibraries)
Seq(JsonFields.preferLocalLibraries -> true.asJson)
else Seq()
val overridesObject = JsonObject(
overrides ++ preferLocalOverride: _*
)
originals.remove(JsonFields.ensoVersion).deepMerge(overridesObject).asJson
}
/** Tries to parse the [[Config]] from a YAML string.
*/
/** Tries to parse the [[Config]] from a YAML string. */
def fromYaml(yamlString: String): Try[Config] = {
yaml.parser.parse(yamlString).flatMap(_.as[Config]).toTry
}
/** Creates a simple edition that just defines the provided engine version.
*
* A compatibility layer for migrating from just specifying the engine
* version to the edition system.
*
* TODO [RW] once the edition is actually used for loading libraries, this
* may need to be revisited, because an edition created in this way will not
* have any libraries present which is highly undesirable. We may either
* remove the compatibility layer and return errors for the old format or
* need to use the latest/default edition or some hardcoded edition for
* compatibility.
*/
def makeCompatibilityEditionFromVersion(
ensoVersion: EnsoVersion
): Editions.RawEdition = Editions.Raw.Edition(
parent = None,
engineVersion = Some(ensoVersion),
repositories = Map(),
libraries = Map()
)
/** A helper method that reconciles the old and new fields of the config
* related to the edition.
*
* If an edition is present, it is just returned as-is. If the engine version
* is specified, a special edition is created that specifies this particular
* engine version and nothing else.
*
* If both fields are defined, an error is raised as the configuration may be
* inconsistent - the `engine-version` field should only be present in old
* configs and after migration to the edition format it should be removed.
*/
private def editionOrVersionBackwardsCompatibility(
edition: Option[Editions.RawEdition],
ensoVersion: Option[EnsoVersion]
): Either[String, Editions.RawEdition] = (edition, ensoVersion) match {
case (Some(_), Some(_)) =>
Left(
s"The deprecated `${JsonFields.ensoVersion}` should not be defined " +
s"if the `${JsonFields.edition}` that replaces it is already defined."
)
case (Some(edition), _) => Right(edition)
case _ =>
val version = ensoVersion.getOrElse(DefaultEnsoVersion)
Right(makeCompatibilityEditionFromVersion(version))
}
}

View File

@ -1,14 +1,13 @@
package org.enso.pkg
import java.io.File
import cats.Show
import scala.jdk.CollectionConverters._
import org.enso.editions.{DefaultEnsoVersion, EnsoVersion}
import org.enso.filesystem.FileSystem
import org.enso.pkg.validation.NameValidation
import java.io.File
import scala.io.Source
import scala.jdk.CollectionConverters._
import scala.util.{Failure, Try, Using}
object CouldNotCreateDirectory extends Exception
@ -202,14 +201,15 @@ class PackageManager[F](implicit val fileSystem: FileSystem[F]) {
authors: List[Contact] = List(),
maintainers: List[Contact] = List()
): Package[F] = {
val edition = Config.makeCompatibilityEditionFromVersion(ensoVersion)
val config = Config(
name = NameValidation.normalizeName(name),
version = version,
ensoVersion = ensoVersion,
license = "",
authors = authors,
maintainers = maintainers,
dependencies = List()
name = NameValidation.normalizeName(name),
version = version,
license = "",
authors = authors,
edition = edition,
preferLocalLibraries = true,
maintainers = maintainers
)
create(root, config)
}

View File

@ -1,6 +1,8 @@
package org.enso.pkg
import io.circe.Json
import io.circe.{Json, JsonObject}
import nl.gn0s1s.bump.SemVer
import org.enso.editions.{DefaultEnsoVersion, SemVerEnsoVersion}
import org.scalatest.{Inside, OptionValues}
import org.scalatest.matchers.should.Matchers
import org.scalatest.wordspec.AnyWordSpec
@ -29,21 +31,52 @@ class ConfigSpec
"deserialize the serialized representation to the original value" in {
val config = Config(
name = "placeholder",
version = "dev",
ensoVersion = DefaultEnsoVersion,
license = "none",
authors = List(),
name = "placeholder",
version = "dev",
edition = Config.makeCompatibilityEditionFromVersion(
SemVerEnsoVersion(SemVer(4, 5, 6))
),
license = "none",
authors = List(),
maintainers = List(
Contact(Some("A"), Some("a@example.com")),
Contact(Some("B"), None),
Contact(None, Some("c@example.com"))
),
dependencies = List(Dependency("dependency", "0.0.0"))
preferLocalLibraries = true
)
val deserialized = Config.fromYaml(config.toYaml).get
val withoutJson = deserialized.copy(originalJson = Json.obj())
val withoutJson = deserialized.copy(originalJson = JsonObject())
withoutJson shouldEqual config
}
"only require the name and use defaults for everything else" in {
val parsed = Config.fromYaml("name: FooBar").get
parsed.name shouldEqual "FooBar"
parsed.edition.engineVersion should contain(DefaultEnsoVersion)
}
"be backwards compatible but correctly migrate to new format on save" in {
val oldFormat =
"""name: FooBar
|enso-version: 1.2.3
|extra-key: extra-value
|""".stripMargin
val parsed = Config.fromYaml(oldFormat).get
parsed.edition.engineVersion should contain(
SemVerEnsoVersion(SemVer(1, 2, 3))
)
val serialized = parsed.toYaml
val parsedAgain = Config.fromYaml(serialized).get
parsedAgain.copy(originalJson = JsonObject()) shouldEqual
parsed.copy(originalJson = JsonObject())
parsedAgain.originalJson("extra-key").flatMap(_.asString) should contain(
"extra-value"
)
}
}
}

View File

@ -3,13 +3,13 @@ package org.enso.projectmanager.infrastructure.languageserver
import java.io.PrintWriter
import java.lang.ProcessBuilder.Redirect
import java.util.concurrent.Executors
import akka.actor.ActorRef
import com.typesafe.scalalogging.Logger
import org.apache.commons.lang3.concurrent.BasicThreadFactory
import org.enso.logger.masking.Masking
import org.enso.loggingservice.LoggingServiceManager
import org.enso.projectmanager.service.versionmanagement.RuntimeVersionManagerFactory
import org.enso.runtimeversionmanager.config.GlobalConfigurationManager
import org.enso.runtimeversionmanager.runner.{LanguageServerOptions, Runner}
import scala.util.Using
@ -80,11 +80,16 @@ object ExecutorWithUnlimitedPool extends LanguageServerExecutor {
rpcPort = rpcPort,
dataPort = dataPort
)
val runner = new Runner(
val configurationManager = new GlobalConfigurationManager(
versionManager,
distributionConfiguration.environment,
descriptor.deferredLoggingServiceEndpoint
distributionConfiguration.distributionManager
)
val runner = new Runner(
runtimeVersionManager = versionManager,
globalConfigurationManager = configurationManager,
editionManager = distributionConfiguration.editionManager,
environment = distributionConfiguration.environment,
loggerConnection = descriptor.deferredLoggingServiceEndpoint
)
val runSettings = runner
.startLanguageServer(

View File

@ -108,7 +108,7 @@ class ProjectFileRepository[
name = pkg.name,
kind = meta.kind,
created = meta.created,
engineVersion = pkg.config.ensoVersion,
edition = pkg.config.edition,
lastOpened = meta.lastOpened,
path = Some(directory.toString),
directoryCreationTime = directoryCreationTime

View File

@ -1,18 +1,18 @@
package org.enso.projectmanager.model
import org.enso.editions.Editions
import java.nio.file.attribute.FileTime
import java.time.OffsetDateTime
import java.util.UUID
import org.enso.pkg.{DefaultEnsoVersion, EnsoVersion}
/** Project entity.
*
* @param id a project id
* @param name a project name
* @param kind a project kind
* @param created a project creation time
* @param engineVersion version of the engine associated with the project
* @param edition the edition configuration associated with the project
* @param lastOpened a project last open time
* @param path a path to the project structure
*/
@ -21,7 +21,7 @@ case class Project(
name: String,
kind: ProjectKind,
created: OffsetDateTime,
engineVersion: EnsoVersion = DefaultEnsoVersion,
edition: Editions.RawEdition,
lastOpened: Option[OffsetDateTime] = None,
path: Option[String] = None,
directoryCreationTime: Option[FileTime] = None

View File

@ -8,7 +8,7 @@ import org.enso.projectmanager.protocol.ProjectManagementApi._
*
* Do not remove this import.
*/
import org.enso.pkg.SemVerJson._
import org.enso.editions.SemVerJson._
object JsonRpc {

View File

@ -1,12 +1,11 @@
package org.enso.projectmanager.protocol
import java.util.UUID
import io.circe.Json
import io.circe.syntax._
import nl.gn0s1s.bump.SemVer
import org.enso.editions.EnsoVersion
import org.enso.jsonrpc.{Error, HasParams, HasResult, Method, Unused}
import org.enso.pkg.EnsoVersion
import org.enso.projectmanager.data.{
EngineVersion,
MissingComponentAction,

View File

@ -1,7 +1,7 @@
package org.enso.projectmanager.requesthandler
import akka.actor._
import org.enso.pkg.DefaultEnsoVersion
import org.enso.editions.DefaultEnsoVersion
import org.enso.projectmanager.control.core.CovariantFlatMap
import org.enso.projectmanager.control.core.syntax._
import org.enso.projectmanager.control.effect.syntax._

View File

@ -1,7 +1,5 @@
package org.enso.projectmanager.service
import java.nio.file.Path
import akka.actor.ActorRef
import nl.gn0s1s.bump.SemVer
import org.enso.projectmanager.control.core.CovariantFlatMap
@ -12,8 +10,11 @@ import org.enso.projectmanager.service.ProjectServiceFailure.ProjectCreateFailed
import org.enso.projectmanager.service.versionmanagement.RuntimeVersionManagerErrorRecoverySyntax._
import org.enso.projectmanager.service.versionmanagement.RuntimeVersionManagerFactory
import org.enso.projectmanager.versionmanagement.DistributionConfiguration
import org.enso.runtimeversionmanager.config.GlobalConfigurationManager
import org.enso.runtimeversionmanager.runner.Runner
import java.nio.file.Path
/** A service for creating new project structures using the runner of the
* specific engine version selected for the project.
*/
@ -36,11 +37,17 @@ class ProjectCreationService[
val versionManager = RuntimeVersionManagerFactory(
distributionConfiguration
).makeRuntimeVersionManager(progressTracker, missingComponentAction)
val configurationManager = new GlobalConfigurationManager(
versionManager,
distributionConfiguration.distributionManager
)
val runner =
new Runner(
versionManager,
distributionConfiguration.environment,
loggingServiceDescriptor.getEndpoint
runtimeVersionManager = versionManager,
globalConfigurationManager = configurationManager,
editionManager = distributionConfiguration.editionManager,
environment = distributionConfiguration.environment,
loggerConnection = loggingServiceDescriptor.getEndpoint
)
val settings =

View File

@ -1,16 +1,16 @@
package org.enso.projectmanager.service
import java.util.UUID
import akka.actor.ActorRef
import cats.MonadError
import nl.gn0s1s.bump.SemVer
import org.enso.editions.{DefaultEnsoVersion, EnsoVersion}
import org.enso.pkg.Config
import org.enso.projectmanager.control.core.syntax._
import org.enso.projectmanager.control.core.{
Applicative,
CovariantFlatMap,
Traverse
}
import org.enso.projectmanager.control.core.syntax._
import org.enso.projectmanager.control.effect.syntax._
import org.enso.projectmanager.control.effect.{ErrorChannel, Sync}
import org.enso.projectmanager.data.{
@ -48,6 +48,8 @@ import org.enso.projectmanager.service.versionmanagement.RuntimeVersionManagerEr
import org.enso.projectmanager.service.versionmanagement.RuntimeVersionManagerFactory
import org.enso.projectmanager.versionmanagement.DistributionConfiguration
import java.util.UUID
/** Implementation of business logic for project management.
*
* @param validator a project validator
@ -87,7 +89,16 @@ class ProjectService[
_ <- validateName(name)
_ <- checkIfNameExists(name)
creationTime <- clock.nowInUtc()
project = Project(projectId, name, UserProject, creationTime)
defaultEdition = Config.makeCompatibilityEditionFromVersion(
DefaultEnsoVersion
)
project = Project(
id = projectId,
name = name,
kind = UserProject,
created = creationTime,
edition = defaultEdition
)
path <- repo.findPathForNewProject(project).mapError(toServiceFailure)
_ <- log.debug(
"Found a path [{}] for a new project [{}, {}].",
@ -275,7 +286,7 @@ class ProjectService[
}
.mapRuntimeManagerErrors(th =>
ProjectOpenFailed(
s"Cannot install the required engine. ${th.getMessage}"
s"Cannot install the required engine. $th"
)
)
@ -285,8 +296,9 @@ class ProjectService[
project: Project,
missingComponentAction: MissingComponentAction
): F[ProjectServiceFailure, RunningLanguageServerInfo] = for {
version <- resolveProjectVersion(project)
version <- configurationService
.resolveEnsoVersion(project.engineVersion)
.resolveEnsoVersion(version)
.mapError { case ConfigurationFileAccessFailure(message) =>
ProjectOpenFailed(
"Could not deduce the default version to use for the project: " +
@ -345,15 +357,17 @@ class ProjectService[
private def resolveProjectMetadata(
project: Project
): F[ProjectServiceFailure, ProjectMetadata] =
configurationService
.resolveEnsoVersion(project.engineVersion)
.mapError { case ConfigurationFileAccessFailure(message) =>
GlobalConfigurationAccessFailure(
"Could not deduce the default version to use for the project: " +
message
)
}
.map(toProjectMetadata(_, project))
for {
version <- resolveProjectVersion(project)
version <- configurationService
.resolveEnsoVersion(version)
.mapError { case ConfigurationFileAccessFailure(message) =>
GlobalConfigurationAccessFailure(
"Could not deduce the default version to use for the project: " +
message
)
}
} yield toProjectMetadata(version, project)
private def toProjectMetadata(
engineVersion: SemVer,
@ -439,4 +453,19 @@ class ProjectService[
log.debug("Project name [{}] validated by [{}].", name, validator)
}
private def resolveProjectVersion(
project: Project
): F[ProjectServiceFailure, EnsoVersion] =
Sync[F]
.blockingOp {
distributionConfiguration.editionManager
.resolveEngineVersion(project.edition)
.get
}
.mapError { error =>
ProjectServiceFailure.GlobalConfigurationAccessFailure(
s"Could not resolve project engine version: ${error.getMessage}"
)
}
}

View File

@ -2,7 +2,7 @@ package org.enso.projectmanager.service.config
import io.circe.Json
import nl.gn0s1s.bump.SemVer
import org.enso.pkg.{DefaultEnsoVersion, EnsoVersion, SemVerEnsoVersion}
import org.enso.editions.{DefaultEnsoVersion, EnsoVersion, SemVerEnsoVersion}
import org.enso.projectmanager.control.core.CovariantFlatMap
import org.enso.projectmanager.control.effect.{ErrorChannel, Sync}
import org.enso.projectmanager.service.config.GlobalConfigServiceFailure.ConfigurationFileAccessFailure

View File

@ -1,7 +1,7 @@
package org.enso.projectmanager.service.config
import nl.gn0s1s.bump.SemVer
import org.enso.pkg.EnsoVersion
import org.enso.editions.EnsoVersion
/** A contract for the Global Config Service.
*

View File

@ -1,17 +1,16 @@
package org.enso.projectmanager.service.versionmanagement
import java.util.UUID
import akka.actor.ActorRef
import com.typesafe.scalalogging.Logger
import nl.gn0s1s.bump.SemVer
import org.enso.cli.task.{ProgressListener, TaskProgress}
import org.enso.distribution.locking.Resource
import org.enso.projectmanager.data.ProgressUnit
import org.enso.runtimeversionmanager.components.{
GraalVMVersion,
RuntimeVersionManagementUserInterface
}
import org.enso.runtimeversionmanager.locking.Resource
import scala.util.{Failure, Success, Try}

View File

@ -2,11 +2,11 @@ package org.enso.projectmanager.service.versionmanagement
import nl.gn0s1s.bump.SemVer
import org.enso.cli.task.TaskProgress
import org.enso.distribution.locking.Resource
import org.enso.runtimeversionmanager.components.{
GraalVMVersion,
RuntimeVersionManagementUserInterface
}
import org.enso.runtimeversionmanager.locking.Resource
/** A simple [[RuntimeVersionManagementUserInterface]] that does not allow to
* install any versions and does not track any progress.

View File

@ -1,7 +1,11 @@
package org.enso.projectmanager.versionmanagement
import com.typesafe.scalalogging.LazyLogging
import org.enso.runtimeversionmanager.Environment
import org.enso.distribution.{DistributionManager, EditionManager, Environment}
import org.enso.distribution.locking.{
ResourceManager,
ThreadSafeFileLockManager
}
import org.enso.runtimeversionmanager.components.{
GraalVMComponentConfiguration,
InstallerKind,
@ -10,14 +14,7 @@ import org.enso.runtimeversionmanager.components.{
RuntimeVersionManagementUserInterface,
RuntimeVersionManager
}
import org.enso.runtimeversionmanager.distribution.{
DistributionManager,
TemporaryDirectoryManager
}
import org.enso.runtimeversionmanager.locking.{
ResourceManager,
ThreadSafeFileLockManager
}
import org.enso.runtimeversionmanager.distribution.TemporaryDirectoryManager
import org.enso.runtimeversionmanager.releases.ReleaseProvider
import org.enso.runtimeversionmanager.releases.engine.{
EngineRelease,
@ -51,6 +48,9 @@ object DefaultDistributionConfiguration
/** @inheritdoc */
lazy val resourceManager = new ResourceManager(lockManager)
/** @inheritdoc */
lazy val editionManager = EditionManager(distributionManager)
/** @inheritdoc */
lazy val temporaryDirectoryManager =
new TemporaryDirectoryManager(distributionManager, resourceManager)

View File

@ -1,15 +1,12 @@
package org.enso.projectmanager.versionmanagement
import org.enso.runtimeversionmanager.Environment
import org.enso.distribution.locking.ResourceManager
import org.enso.distribution.{DistributionManager, EditionManager, Environment}
import org.enso.runtimeversionmanager.components.{
RuntimeVersionManagementUserInterface,
RuntimeVersionManager
}
import org.enso.runtimeversionmanager.distribution.{
DistributionManager,
TemporaryDirectoryManager
}
import org.enso.runtimeversionmanager.locking.ResourceManager
import org.enso.runtimeversionmanager.distribution.TemporaryDirectoryManager
import org.enso.runtimeversionmanager.releases.ReleaseProvider
import org.enso.runtimeversionmanager.releases.engine.EngineRelease
import org.enso.runtimeversionmanager.runner.JVMSettings
@ -31,6 +28,9 @@ trait DistributionConfiguration {
/** A [[ResourceManager]] instance. */
def resourceManager: ResourceManager
/** An [[EditionManager]] instance. */
def editionManager: EditionManager
/** A [[TemporaryDirectoryManager]] instance. */
def temporaryDirectoryManager: TemporaryDirectoryManager

View File

@ -4,13 +4,13 @@ import java.io.File
import java.nio.file.{Files, Path}
import java.time.{OffsetDateTime, ZoneOffset}
import java.util.UUID
import akka.testkit.TestActors.blackholeProps
import akka.testkit._
import io.circe.Json
import io.circe.parser.parse
import nl.gn0s1s.bump.SemVer
import org.apache.commons.io.FileUtils
import org.enso.distribution.OS
import org.enso.jsonrpc.test.JsonRpcServerTestKit
import org.enso.jsonrpc.{ClientControllerFactory, Protocol}
import org.enso.loggingservice.printers.StderrPrinterWithColors
@ -39,7 +39,6 @@ import org.enso.projectmanager.service.{
ProjectService
}
import org.enso.projectmanager.test.{ObservableGenerator, ProgrammableClock}
import org.enso.runtimeversionmanager.OS
import org.enso.runtimeversionmanager.test.FakeReleases
import org.scalatest.BeforeAndAfterAll
import pureconfig.ConfigSource

View File

@ -5,7 +5,7 @@ import akka.testkit.TestDuration
import io.circe.Json
import io.circe.syntax._
import io.circe.literal._
import org.enso.pkg.SemVerJson._
import org.enso.editions.SemVerJson._
import io.circe.parser.parse
import nl.gn0s1s.bump.SemVer
import org.enso.projectmanager.data.{MissingComponentAction, Socket}

View File

@ -1,7 +1,9 @@
package org.enso.projectmanager
import java.nio.file.Path
import org.enso.distribution.{DistributionManager, EditionManager}
import org.enso.distribution.locking.ResourceManager
import java.nio.file.Path
import org.enso.projectmanager.versionmanagement.DistributionConfiguration
import org.enso.runtimeversionmanager.components.{
GraalVMComponentConfiguration,
@ -9,11 +11,7 @@ import org.enso.runtimeversionmanager.components.{
RuntimeVersionManagementUserInterface,
RuntimeVersionManager
}
import org.enso.runtimeversionmanager.distribution.{
DistributionManager,
TemporaryDirectoryManager
}
import org.enso.runtimeversionmanager.locking.ResourceManager
import org.enso.runtimeversionmanager.distribution.TemporaryDirectoryManager
import org.enso.runtimeversionmanager.releases.engine.{
EngineRelease,
EngineReleaseProvider
@ -66,6 +64,8 @@ class TestDistributionConfiguration(
lazy val resourceManager = new ResourceManager(lockManager)
lazy val editionManager: EditionManager = EditionManager(distributionManager)
lazy val temporaryDirectoryManager =
new TemporaryDirectoryManager(distributionManager, resourceManager)

View File

@ -6,7 +6,7 @@ import nl.gn0s1s.bump.SemVer
import org.enso.projectmanager.data.MissingComponentAction
import org.enso.projectmanager.{BaseServerSpec, ProjectManagementOps}
import org.enso.testkit.RetrySpec
import org.enso.pkg.SemVerJson._
import org.enso.editions.SemVerJson._
class ProjectCreateMissingComponentsSpec
extends BaseServerSpec

View File

@ -7,7 +7,7 @@ import java.util.UUID
import io.circe.literal._
import nl.gn0s1s.bump.SemVer
import org.apache.commons.io.FileUtils
import org.enso.pkg.SemVerJson._
import org.enso.editions.SemVerJson._
import org.enso.projectmanager.{BaseServerSpec, ProjectManagementOps}
import org.enso.testkit.{FlakySpec, RetrySpec}

Some files were not shown because too many files have changed in this diff Show More