mirror of
https://github.com/enso-org/enso.git
synced 2024-09-11 21:27:15 +03:00
Run components through the launcher (#1073)
This commit is contained in:
parent
6da3b9252f
commit
c979938527
4
.github/workflows/release.yml
vendored
4
.github/workflows/release.yml
vendored
@ -105,7 +105,7 @@ jobs:
|
||||
shell: bash
|
||||
run: |
|
||||
chmod +x enso
|
||||
DIST_VERSION=$(./enso version --json | jq -r '.version')
|
||||
DIST_VERSION=$(./enso version --json --only-launcher | jq -r '.version')
|
||||
echo ::set-env name=DIST_VERSION::$DIST_VERSION
|
||||
|
||||
- name: Prepare Distribution Version (Windows)
|
||||
@ -113,7 +113,7 @@ jobs:
|
||||
if: runner.os == 'Windows'
|
||||
shell: bash
|
||||
run: |
|
||||
DIST_VERSION=$(./enso.exe version --json | jq -r '.version')
|
||||
DIST_VERSION=$(./enso.exe version --json --only-launcher | jq -r '.version')
|
||||
echo ::set-env name=DIST_VERSION::$DIST_VERSION
|
||||
|
||||
# Currently the only architecture supported by Github runners is amd64
|
||||
|
9
.github/workflows/scala.yml
vendored
9
.github/workflows/scala.yml
vendored
@ -201,18 +201,23 @@ jobs:
|
||||
run: sbt -no-colors syntaxJS/fullOptJS
|
||||
|
||||
# Prepare distributions
|
||||
# The version used in filenames is based on the version of the launcher.
|
||||
# Currently launcher and engine versions are tied to each other so they
|
||||
# can be used interchangeably like this. If in the future the versions
|
||||
# become independent, this may require updating to use proper versions
|
||||
# for each component.
|
||||
- name: Prepare Distribution Version (Unix)
|
||||
if: runner.os != 'Windows'
|
||||
shell: bash
|
||||
run: |
|
||||
DIST_VERSION=$(./enso version --json | jq -r '.version')
|
||||
DIST_VERSION=$(./enso version --json --only-launcher | jq -r '.version')
|
||||
echo ::set-env name=DIST_VERSION::$DIST_VERSION
|
||||
|
||||
- name: Prepare Distribution Version (Windows)
|
||||
if: runner.os == 'Windows'
|
||||
shell: bash
|
||||
run: |
|
||||
DIST_VERSION=$(./enso.exe version --json | jq -r '.version')
|
||||
DIST_VERSION=$(./enso.exe version --json --only-launcher | jq -r '.version')
|
||||
echo ::set-env name=DIST_VERSION::$DIST_VERSION
|
||||
|
||||
# Currently the only architecture supported by Github runners is amd64
|
||||
|
@ -485,7 +485,8 @@ lazy val pkg = (project in file("lib/scala/pkg"))
|
||||
mainClass in (Compile, run) := Some("org.enso.pkg.Main"),
|
||||
version := "0.1",
|
||||
libraryDependencies ++= circe ++ Seq(
|
||||
"io.circe" %% "circe-yaml" % circeYamlVersion, // separate from other circe deps because its independent project with its own versioning
|
||||
"nl.gn0s1s" %% "bump" % bumpVersion,
|
||||
"io.circe" %% "circe-yaml" % circeYamlVersion, // separate from other circe deps because its independent project with its own versionin
|
||||
"commons-io" % "commons-io" % commonsIoVersion
|
||||
)
|
||||
)
|
||||
|
@ -1 +1,9 @@
|
||||
minimum-launcher-version: 0.0.1
|
||||
jvm-options:
|
||||
- value: "-Dpolyglot.engine.IterativePartialEscape=true"
|
||||
- value: "-Dtruffle.class.path.append=$enginePackagePath\\component\\runtime.jar"
|
||||
os: "windows"
|
||||
- value: "-Dtruffle.class.path.append=$enginePackagePath/component/runtime.jar"
|
||||
os: "linux"
|
||||
- value: "-Dtruffle.class.path.append=$enginePackagePath/component/runtime.jar"
|
||||
os: "macos"
|
||||
|
@ -1,5 +1,6 @@
|
||||
license: APLv2
|
||||
name: Base
|
||||
enso-version: default
|
||||
version: "0.0.1"
|
||||
author: "Enso Team <contact@enso.org>"
|
||||
maintainer: "Enso Team <contact@enso.org>"
|
||||
|
@ -259,13 +259,22 @@ command.
|
||||
Tells the launcher to use the default JVM (based on `JAVA_HOME`) instead of the
|
||||
managed one. Will not work if the set-up JVM version is not GraalVM.
|
||||
|
||||
## JVM Options
|
||||
## Options From Newer Versions
|
||||
|
||||
For commands that launch an Enso component inside a JVM (`repl`, `run` and
|
||||
`language-server`), parameters that the launcher does not know about (for
|
||||
example introduced in versions of Enso newer than the launcher knows about) may
|
||||
be passed after a double dash (`--`), i.e. `enso repl -- --someUnknownFlag`.
|
||||
|
||||
## JVM Options
|
||||
|
||||
If an environment variable `ENSO_JVM_OPTS` is defined, JVM options defined there
|
||||
are passed to the launcher JVM.
|
||||
|
||||
> Note: Currently the `ENSO_JVM_OPTS` are parsed by splitting on the space
|
||||
> character, so individual options listed in this environment variable should
|
||||
> not contain spaces or they may be interpreted incorrectly.
|
||||
|
||||
Moreover, it is possible to pass parameters to the JVM that is used to launch
|
||||
these components, which may be helpful with debugging. A parameter of the form
|
||||
`--jvm.argumentName=argumentValue` will be passed to the JVM as
|
||||
|
@ -92,7 +92,7 @@ configuration based on user's config.
|
||||
|
||||
### Per-Project Enso Version
|
||||
|
||||
Project configuration can specify the exact Enso version that should be used
|
||||
Project configuration must specify the exact Enso version that should be used
|
||||
inside that project. The launcher automatically detects if it is in a project
|
||||
(by traversing the directory structure). The current project can also be
|
||||
specified by the `--path` parameter. All components launched inside a project
|
||||
|
@ -95,6 +95,7 @@ The following is an example of this manifest file.
|
||||
license: MIT
|
||||
name: My_Package
|
||||
version: 1.0.1
|
||||
enso-version: 0.1.0
|
||||
author: "John Doe <john.doe@example.com>"
|
||||
maintainer: "Jane Doe <jane.doe@example.com>"
|
||||
resolver: lts-1.2.0
|
||||
@ -117,6 +118,13 @@ fields cannot be published.
|
||||
package. Defaults to `None`, meaning the package is not safe for use by third
|
||||
parties.
|
||||
|
||||
#### 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.
|
||||
|
||||
#### version
|
||||
|
||||
**Optional (required for publishing)** _String_: The
|
||||
|
@ -105,12 +105,31 @@ root of an Enso version package. It has at least the following fields:
|
||||
- `graal-java-version` - as GraalVM versions may have different variants for
|
||||
different Java versions, this specifies which variant to use.
|
||||
|
||||
It can also contain the following additional fields:
|
||||
|
||||
- `jvm-options` - specifies a list of options that should be passed to the JVM
|
||||
running the engine. These options can be used to fine-tune version specific
|
||||
optimization settings etc. Each option must have a key called `value` which
|
||||
specifies what option should be passed. That value can include a variable
|
||||
`$enginePackagePath` which is substituted with the absolute path to the root
|
||||
of the engine package that is being launched. Optionally, the option may
|
||||
define `os` which will restrict this option only to the provided operating
|
||||
system. Possible `os` values are `linux`, `macos` and `windows`.
|
||||
|
||||
For example:
|
||||
|
||||
```yaml
|
||||
minimum-launcher-version: 0.0.1
|
||||
graal-vm-version: 20.1.0
|
||||
graal-java-version: 11
|
||||
jvm-options:
|
||||
- value: "-Dpolyglot.engine.IterativePartialEscape=true"
|
||||
- value: "-Dtruffle.class.path.append=$enginePackagePath\\component\\runtime.jar"
|
||||
os: "windows"
|
||||
- value: "-Dtruffle.class.path.append=$enginePackagePath/component/runtime.jar"
|
||||
os: "linux"
|
||||
- value: "-Dtruffle.class.path.append=$enginePackagePath/component/runtime.jar"
|
||||
os: "macos"
|
||||
```
|
||||
|
||||
The `minimum-launcher-version` should be updated whenever a new version of Enso
|
||||
|
@ -118,7 +118,7 @@ object DirectoriesConfig {
|
||||
*
|
||||
* @param contentRoots a mapping between content root id and absolute path to
|
||||
* the content root
|
||||
* @param fileManager the file manater config
|
||||
* @param fileManager the file manager config
|
||||
* @param pathWatcher the path watcher config
|
||||
* @param executionContext the executionContext config
|
||||
* @param directories the configuration of internal directories
|
||||
|
@ -1,15 +1,43 @@
|
||||
package org.enso.launcher
|
||||
|
||||
import nl.gn0s1s.bump.SemVer
|
||||
import org.enso.launcher.components.ComponentsManager
|
||||
|
||||
/**
|
||||
* Manages the global configuration of the distribution which includes the
|
||||
* default engine version and default project metadata used for new projects.
|
||||
*
|
||||
* TODO [RW] This is a stub. It will be implemented in #977
|
||||
*/
|
||||
class GlobalConfigurationManager(componentsManager: ComponentsManager) {
|
||||
|
||||
/**
|
||||
* Returns the default Enso version that should be used when running Enso
|
||||
* outside a project and when creating a new project.
|
||||
*
|
||||
* The default can be set by `enso default <version>` (TODO [RW] #977). If
|
||||
* the default is not set, the latest installed version is used. If no
|
||||
* versions are installed, the release provider is queried for the latest
|
||||
* available version.
|
||||
*/
|
||||
def defaultVersion: SemVer = {
|
||||
val latestInstalled =
|
||||
componentsManager.listInstalledEngines().map(_.version).sorted.lastOption
|
||||
latestInstalled.getOrElse {
|
||||
val latestAvailable = componentsManager.fetchLatestEngineVersion()
|
||||
Logger.warn(
|
||||
s"No Enso versions installed, defaulting to the latest available " +
|
||||
s"release: $latestAvailable."
|
||||
)
|
||||
latestAvailable
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object GlobalConfigurationManager {
|
||||
|
||||
/**
|
||||
* Name of the main global configuration file.
|
||||
*/
|
||||
def globalConfigName: String = "global-config.yml"
|
||||
val globalConfigName: String = "global-config.yml"
|
||||
}
|
||||
|
@ -2,13 +2,19 @@ package org.enso.launcher
|
||||
|
||||
import java.nio.file.Path
|
||||
|
||||
import org.enso.launcher.installation.DistributionManager
|
||||
import org.enso.pkg.PackageManager
|
||||
import org.enso.version.{VersionDescription, VersionDescriptionParameter}
|
||||
import buildinfo.Info
|
||||
import nl.gn0s1s.bump.SemVer
|
||||
import org.enso.launcher.cli.GlobalCLIOptions
|
||||
import org.enso.launcher.components.DefaultComponentsManager
|
||||
import org.enso.launcher.components.runner.{
|
||||
JVMSettings,
|
||||
LanguageServerOptions,
|
||||
Runner,
|
||||
WhichEngine
|
||||
}
|
||||
import org.enso.launcher.installation.DistributionManager
|
||||
import org.enso.launcher.project.ProjectManager
|
||||
import org.enso.version.{VersionDescription, VersionDescriptionParameter}
|
||||
|
||||
/**
|
||||
* Implements launcher commands that are run from CLI and can be affected by
|
||||
@ -18,6 +24,30 @@ import org.enso.launcher.components.DefaultComponentsManager
|
||||
*/
|
||||
case class Launcher(cliOptions: GlobalCLIOptions) {
|
||||
private lazy val componentsManager = DefaultComponentsManager(cliOptions)
|
||||
private lazy val configurationManager =
|
||||
new GlobalConfigurationManager(componentsManager)
|
||||
private lazy val projectManager = new ProjectManager(configurationManager)
|
||||
private lazy val runner =
|
||||
new Runner(
|
||||
projectManager,
|
||||
configurationManager,
|
||||
componentsManager,
|
||||
Environment
|
||||
)
|
||||
|
||||
/**
|
||||
* Creates a new project with the given `name` in the given `path`.
|
||||
*
|
||||
* If `path` is not set, the project is created in a directory called `name`
|
||||
* in the current directory.
|
||||
*
|
||||
* TODO [RW] this is not the final implementation, it will be finished in
|
||||
* #977
|
||||
*/
|
||||
def newProject(name: String, path: Option[Path]): Unit = {
|
||||
val actualPath = path.getOrElse(Launcher.workingDirectory.resolve(name))
|
||||
projectManager.newProject(name, actualPath)
|
||||
}
|
||||
|
||||
/**
|
||||
* Prints a list of installed engines.
|
||||
@ -78,7 +108,7 @@ case class Launcher(cliOptions: GlobalCLIOptions) {
|
||||
*/
|
||||
def installLatestEngine(): Unit = {
|
||||
val latest = componentsManager.fetchLatestEngineVersion()
|
||||
Logger.info(s"Installing Enso engine $latest")
|
||||
Logger.info(s"Installing Enso engine $latest.")
|
||||
installEngine(latest)
|
||||
}
|
||||
|
||||
@ -90,19 +120,175 @@ case class Launcher(cliOptions: GlobalCLIOptions) {
|
||||
def uninstallEngine(version: SemVer): Unit =
|
||||
componentsManager.uninstallEngine(version)
|
||||
|
||||
/**
|
||||
* Runs the Enso REPL.
|
||||
*
|
||||
* If ran outside of a project, uses the default configured version. If run
|
||||
* inside a project or provided with an explicit projectPath, the Enso
|
||||
* version associated with the project is run.
|
||||
*
|
||||
* @param projectPath if provided, the REPL is run in context of that project
|
||||
* @param versionOverride if provided, overrides the default engine version
|
||||
* that would have been used
|
||||
* @param useSystemJVM if set, forces to use the default configured JVM,
|
||||
* instead of the JVM associated with the engine version
|
||||
* @param jvmOpts additional options to pass to the launched JVM
|
||||
* @param additionalArguments additional arguments to pass to the runner
|
||||
*/
|
||||
def runRepl(
|
||||
pathHint: Option[Path],
|
||||
jvmArguments: Seq[(String, String)],
|
||||
projectPath: Option[Path],
|
||||
versionOverride: Option[SemVer],
|
||||
useSystemJVM: Boolean,
|
||||
jvmOpts: Seq[(String, String)],
|
||||
additionalArguments: Seq[String]
|
||||
): Unit = {
|
||||
// TODO [RW] this is just a stub, it will be implemented in #976
|
||||
val path = pathHint.getOrElse(Path.of(".")).toAbsolutePath
|
||||
val detectedVersion = SemVer(0, 1, 0) // TODO [RW] default version etc.
|
||||
val engine = componentsManager.findOrInstallEngine(detectedVersion)
|
||||
val runtime = componentsManager.findOrInstallRuntime(engine)
|
||||
println(s"Will launch the REPL in $path")
|
||||
println(s"with $engine with additional arguments $additionalArguments")
|
||||
println(s"using $runtime with JVM arguments $jvmArguments")
|
||||
val exitCode = runner
|
||||
.createCommand(
|
||||
runner.repl(projectPath, versionOverride, additionalArguments).get,
|
||||
JVMSettings(useSystemJVM, jvmOpts)
|
||||
)
|
||||
.run()
|
||||
.get
|
||||
sys.exit(exitCode)
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs an Enso script or project.
|
||||
*
|
||||
* If ran inside a project without a path, or with a path pointing to a
|
||||
* project, runs that project. If the provided path points to a file, that
|
||||
* file is executed as an Enso script. If the file is located inside of a
|
||||
* project, it is executed in the context of that project. Otherwise it is
|
||||
* run as a standalone script and the default engine version is used.
|
||||
*
|
||||
* @param path specifies what to run
|
||||
* @param versionOverride if provided, overrides the default engine version
|
||||
* that would have been used
|
||||
* @param useSystemJVM if set, forces to use the default configured JVM,
|
||||
* instead of the JVM associated with the engine version
|
||||
* @param jvmOpts additional options to pass to the launched JVM
|
||||
* @param additionalArguments additional arguments to pass to the runner
|
||||
*/
|
||||
def runRun(
|
||||
path: Option[Path],
|
||||
versionOverride: Option[SemVer],
|
||||
useSystemJVM: Boolean,
|
||||
jvmOpts: Seq[(String, String)],
|
||||
additionalArguments: Seq[String]
|
||||
): Unit = {
|
||||
val exitCode = runner
|
||||
.createCommand(
|
||||
runner.run(path, versionOverride, additionalArguments).get,
|
||||
JVMSettings(useSystemJVM, jvmOpts)
|
||||
)
|
||||
.run()
|
||||
.get
|
||||
sys.exit(exitCode)
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the Language Server.
|
||||
*
|
||||
* Unless overridden, uses the Enso version associated with the project
|
||||
* located at `options.path`.
|
||||
*
|
||||
* @param options configuration required by the language server
|
||||
* @param versionOverride if provided, overrides the default engine version
|
||||
* that would have been used
|
||||
* @param useSystemJVM if set, forces to use the default configured JVM,
|
||||
* instead of the JVM associated with the engine version
|
||||
* @param jvmOpts additional options to pass to the launched JVM
|
||||
* @param additionalArguments additional arguments to pass to the runner
|
||||
*/
|
||||
def runLanguageServer(
|
||||
options: LanguageServerOptions,
|
||||
versionOverride: Option[SemVer],
|
||||
useSystemJVM: Boolean,
|
||||
jvmOpts: Seq[(String, String)],
|
||||
additionalArguments: Seq[String]
|
||||
): Unit = {
|
||||
val exitCode = runner
|
||||
.createCommand(
|
||||
runner
|
||||
.languageServer(options, versionOverride, additionalArguments)
|
||||
.get,
|
||||
JVMSettings(useSystemJVM, jvmOpts)
|
||||
)
|
||||
.run()
|
||||
.get
|
||||
sys.exit(exitCode)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the default Enso version.
|
||||
*/
|
||||
def setDefaultVersion(version: SemVer): Unit = {
|
||||
val _ = version
|
||||
Logger.error("This feature is not implemented yet.")
|
||||
sys.exit(1)
|
||||
}
|
||||
|
||||
/**
|
||||
* Prints the default Enso version.
|
||||
*/
|
||||
def printDefaultVersion(): Unit = {
|
||||
println(configurationManager.defaultVersion)
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays the version string of the launcher.
|
||||
*
|
||||
* @param useJSON specifies whether the output should use JSON or a
|
||||
* human-readable format
|
||||
* @param hideEngineVersion if set, does not look for installed engines to
|
||||
* display the current version; this can be used to
|
||||
* avoid making network requests
|
||||
*/
|
||||
def displayVersion(
|
||||
useJSON: Boolean,
|
||||
hideEngineVersion: Boolean = false
|
||||
): Unit = {
|
||||
val runtimeVersionParameter =
|
||||
if (hideEngineVersion) None else Some(getEngineVersion(useJSON))
|
||||
|
||||
val versionDescription = VersionDescription.make(
|
||||
"Enso Launcher",
|
||||
includeRuntimeJVMInfo = false,
|
||||
additionalParameters = runtimeVersionParameter.toSeq
|
||||
)
|
||||
|
||||
println(versionDescription.asString(useJSON))
|
||||
}
|
||||
|
||||
private def getEngineVersion(
|
||||
useJSON: Boolean
|
||||
): VersionDescriptionParameter = {
|
||||
val (runtimeVersionRunSettings, whichEngine) = runner.version(useJSON).get
|
||||
|
||||
val isEngineInstalled =
|
||||
componentsManager.findEngine(runtimeVersionRunSettings.version).isDefined
|
||||
val runtimeVersionString = if (isEngineInstalled) {
|
||||
val runtimeVersionCommand = runner.createCommand(
|
||||
runtimeVersionRunSettings,
|
||||
JVMSettings(useSystemJVM = false, jvmOptions = Seq.empty)
|
||||
)
|
||||
|
||||
val output = runtimeVersionCommand.captureOutput().get
|
||||
if (useJSON) output else "\n" + output.stripTrailing()
|
||||
} else {
|
||||
if (useJSON) "null"
|
||||
else "Not installed."
|
||||
}
|
||||
|
||||
VersionDescriptionParameter(
|
||||
humanReadableName = whichEngine match {
|
||||
case WhichEngine.FromProject(name) =>
|
||||
s"Enso engine from project $name"
|
||||
case WhichEngine.Default => "Current default Enso engine"
|
||||
},
|
||||
jsonName = "runtime",
|
||||
value = runtimeVersionString
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -118,48 +304,8 @@ object Launcher {
|
||||
throw new IllegalStateException("Cannot parse the built-in version.")
|
||||
}
|
||||
|
||||
private val packageManager = PackageManager.Default
|
||||
private val workingDirectory: Path = Path.of(".")
|
||||
|
||||
/**
|
||||
* Creates a new project with the given `name` in the given `path`.
|
||||
*
|
||||
* If `path` is not set, the project is created in a directory called `name`
|
||||
* in the current directory.
|
||||
*
|
||||
* TODO [RW] this is not the final implementation, it will be finished in
|
||||
* #977
|
||||
*/
|
||||
def newProject(name: String, path: Option[Path]): Unit = {
|
||||
val actualPath = path.getOrElse(workingDirectory.resolve(name))
|
||||
packageManager.create(actualPath.toFile, name)
|
||||
Logger.info(s"Project created in $actualPath")
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays the version string of the launcher.
|
||||
*
|
||||
* @param useJSON specifies whether the output should use JSON or a
|
||||
* human-readable format
|
||||
*/
|
||||
def displayVersion(useJSON: Boolean): Unit = {
|
||||
val runtimeVersionParameter = VersionDescriptionParameter(
|
||||
humanReadableName = "Currently selected Enso version",
|
||||
humandReadableValue =
|
||||
"\nRuntime component is not yet implemented in the launcher.",
|
||||
jsonName = "runtime",
|
||||
jsonValue = "\"<not implemented yet>\"" // TODO [RW] add with #976
|
||||
)
|
||||
|
||||
val versionDescription = VersionDescription.make(
|
||||
"Enso Launcher",
|
||||
includeRuntimeJVMInfo = false,
|
||||
additionalParameters = Seq(runtimeVersionParameter)
|
||||
)
|
||||
|
||||
println(versionDescription.asString(useJSON))
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the launcher is running in portable mode and exits if it is not.
|
||||
*/
|
||||
|
@ -1,5 +1,7 @@
|
||||
package org.enso.launcher
|
||||
|
||||
import java.io.PrintStream
|
||||
|
||||
/**
|
||||
* This is a temporary object that should be at some point replaced with the
|
||||
* actual logging service.
|
||||
@ -8,17 +10,17 @@ package org.enso.launcher
|
||||
* is implemented in #1031
|
||||
*/
|
||||
object Logger {
|
||||
private case class Level(name: String, level: Int)
|
||||
private val Debug = Level("debug", 1)
|
||||
private val Info = Level("info", 2)
|
||||
private val Warning = Level("warn", 3)
|
||||
private val Error = Level("error", 4)
|
||||
private case class Level(name: String, level: Int, stream: PrintStream)
|
||||
private val Debug = Level("debug", 1, System.err)
|
||||
private val Info = Level("info", 2, System.out)
|
||||
private val Warning = Level("warn", 3, System.err)
|
||||
private val Error = Level("error", 4, System.err)
|
||||
|
||||
private var logLevel = Info
|
||||
private def log(level: Level, msg: => String): Unit =
|
||||
if (level.level >= logLevel.level) {
|
||||
System.out.println(s"[${level.name}] $msg")
|
||||
System.out.flush()
|
||||
level.stream.println(s"[${level.name}] $msg")
|
||||
level.stream.flush()
|
||||
}
|
||||
|
||||
/**
|
||||
@ -68,6 +70,11 @@ object Logger {
|
||||
/**
|
||||
* Runs the provided action with a log level that will allow only for errors
|
||||
* and returns its result.
|
||||
*
|
||||
* Warning: This function is not thread safe, so using it in tests that are
|
||||
* run in parallel without forking may lead to an inconsistency in logging.
|
||||
* This is just a *temporary* solution until a fully-fledged logging service
|
||||
* is developed #1031.
|
||||
*/
|
||||
def suppressWarnings[R](action: => R): R = {
|
||||
val oldLevel = logLevel
|
||||
|
@ -1,17 +1,66 @@
|
||||
package org.enso.launcher
|
||||
|
||||
import io.circe.{Decoder, DecodingFailure}
|
||||
|
||||
/**
|
||||
* Represents one of the supported platforms (operating systems).
|
||||
*/
|
||||
sealed trait OS {
|
||||
def name: String
|
||||
|
||||
/**
|
||||
* Name of this operating system as included in the configuration.
|
||||
*/
|
||||
def configName: String
|
||||
|
||||
/**
|
||||
* Checks if the provided `os.name` matches this operating system.
|
||||
*/
|
||||
def matches(osName: String): Boolean = osName.toLowerCase.contains(configName)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gathers helper functions useful for dealing with platform-specific
|
||||
* behaviour.
|
||||
*/
|
||||
object OS {
|
||||
|
||||
/**
|
||||
* Represents the Linux operating system.
|
||||
*/
|
||||
case object Linux extends OS {
|
||||
def name: String = "linux"
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
def configName: String = "linux"
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the macOS operating system.
|
||||
*/
|
||||
case object MacOS extends OS {
|
||||
def name: String = "mac"
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
def configName: String = "macos"
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
override def matches(osName: String): Boolean =
|
||||
osName.toLowerCase.contains("mac")
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the Windows operating system.
|
||||
*/
|
||||
case object Windows extends OS {
|
||||
def name: String = "windows"
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
def configName: String = "windows"
|
||||
}
|
||||
|
||||
/**
|
||||
@ -30,12 +79,15 @@ object OS {
|
||||
|
||||
private val ENSO_OPERATING_SYSTEM = "ENSO_OPERATING_SYSTEM"
|
||||
|
||||
private val knownOS = Seq(Linux, MacOS, Windows)
|
||||
private lazy val knownOSPossibleValuesString =
|
||||
knownOS.map(os => s"`${os.configName}`").mkString(", ")
|
||||
|
||||
private def detectOS: OS = {
|
||||
val knownOS = Seq(Linux, MacOS, Windows)
|
||||
val overridenName = Option(System.getenv(ENSO_OPERATING_SYSTEM))
|
||||
overridenName match {
|
||||
case Some(value) =>
|
||||
knownOS.find(_.name == value.toLowerCase) match {
|
||||
knownOS.find(value.toLowerCase == _.configName) match {
|
||||
case Some(overriden) =>
|
||||
Logger.debug(
|
||||
s"OS overriden by $ENSO_OPERATING_SYSTEM to $overriden."
|
||||
@ -43,16 +95,15 @@ object OS {
|
||||
return overriden
|
||||
case None =>
|
||||
Logger.warn(
|
||||
s"$ENSO_OPERATING_SYSTEM is set to an unknown value $value, " +
|
||||
s"ignoring."
|
||||
s"$ENSO_OPERATING_SYSTEM is set to an unknown value `$value`, " +
|
||||
s"ignoring. Possible values are $knownOSPossibleValuesString."
|
||||
)
|
||||
}
|
||||
case None =>
|
||||
}
|
||||
|
||||
val name = System.getProperty("os.name").toLowerCase
|
||||
def nameMatches(os: OS): Boolean = name.contains(os.name)
|
||||
val possibleOS = knownOS.filter(nameMatches)
|
||||
val name = System.getProperty("os.name")
|
||||
val possibleOS = knownOS.filter(_.matches(name))
|
||||
if (possibleOS.length == 1) {
|
||||
possibleOS.head
|
||||
} else {
|
||||
@ -61,7 +112,7 @@ object OS {
|
||||
s"the OS you are running is supported. You can try to manually " +
|
||||
s"override the operating system detection by setting an environment " +
|
||||
s"variable `$ENSO_OPERATING_SYSTEM` to one of the possible values " +
|
||||
s"`linux`, `mac`, `windows` depending on the system that your OS " +
|
||||
s"$knownOSPossibleValuesString depending on the system that your OS " +
|
||||
s"most behaves like."
|
||||
)
|
||||
throw new IllegalStateException(
|
||||
@ -90,4 +141,20 @@ object OS {
|
||||
*/
|
||||
def executableName(baseName: String): String =
|
||||
if (isWindows) baseName + ".exe" else baseName
|
||||
|
||||
/**
|
||||
* A [[Decoder]] instance allowing to parse the OS name from JSON and YAML
|
||||
* configuration.
|
||||
*/
|
||||
implicit val decoder: Decoder[OS] = { json =>
|
||||
json.as[String].flatMap { string =>
|
||||
knownOS.find(_.configName == string).toRight {
|
||||
DecodingFailure(
|
||||
s"`$string` is not a valid OS name. " +
|
||||
s"Possible values are $knownOSPossibleValuesString.",
|
||||
json.history
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -8,6 +8,7 @@ import nl.gn0s1s.bump.SemVer
|
||||
import org.enso.cli.Opts.implicits._
|
||||
import org.enso.cli._
|
||||
import org.enso.launcher.cli.Arguments._
|
||||
import org.enso.launcher.components.runner.LanguageServerOptions
|
||||
import org.enso.launcher.installation.DistributionInstaller.BundleAction
|
||||
import org.enso.launcher.installation.{
|
||||
DistributionInstaller,
|
||||
@ -33,8 +34,19 @@ object Main {
|
||||
"version",
|
||||
"Print version of the launcher and currently selected Enso distribution."
|
||||
) {
|
||||
jsonFlag(showInUsage = true) map { useJSON => (_: Config) =>
|
||||
Launcher.displayVersion(useJSON)
|
||||
val onlyLauncherFlag = Opts.flag(
|
||||
"only-launcher",
|
||||
"If set, shows only the launcher version, skipping checking for " +
|
||||
"engine versions which may involve network requests depending on " +
|
||||
"configuration.",
|
||||
showInUsage = true
|
||||
)
|
||||
(jsonFlag(showInUsage = true), onlyLauncherFlag) mapN {
|
||||
(useJSON, onlyLauncher) => (config: Config) =>
|
||||
Launcher(config).displayVersion(
|
||||
useJSON,
|
||||
hideEngineVersion = onlyLauncher
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -47,16 +59,30 @@ object Main {
|
||||
"called NAME is created in the current directory."
|
||||
)
|
||||
|
||||
(nameOpt, pathOpt) mapN { (name, path) => (_: Config) =>
|
||||
Launcher.newProject(name, path)
|
||||
(nameOpt, pathOpt) mapN { (name, path) => (config: Config) =>
|
||||
Launcher(config).newProject(name, path)
|
||||
}
|
||||
}
|
||||
|
||||
private def jvmArgs =
|
||||
private def jvmOpts =
|
||||
Opts.prefixedParameters(
|
||||
"jvm",
|
||||
"These parameters will be passed to the launched JVM as -DKEY=VALUE."
|
||||
)
|
||||
private def systemJVMOverride =
|
||||
Opts.flag(
|
||||
"use-system-jvm",
|
||||
"Setting this flag runs the Enso engine using the system-configured " +
|
||||
"JVM instead of the one managed by the launcher. " +
|
||||
"Advanced option, use carefully.",
|
||||
showInUsage = false
|
||||
)
|
||||
private def versionOverride =
|
||||
Opts.optionalParameter[SemVer](
|
||||
"use-enso-version",
|
||||
"VERSION",
|
||||
"Overrides the Enso version that would normally be used"
|
||||
)
|
||||
|
||||
private def runCommand: Command[Config => Unit] =
|
||||
Command(
|
||||
@ -68,18 +94,29 @@ object Main {
|
||||
) {
|
||||
val pathOpt = Opts.optionalArgument[Path](
|
||||
"PATH",
|
||||
"If PATH points to a file, that file is run as a script. " +
|
||||
"If it points to a directory, the project from that directory is " +
|
||||
"run. If a PATH is not provided, a project in the current working " +
|
||||
"directory is run."
|
||||
"If PATH points to a file, that file is run as a script (if that " +
|
||||
"script is located inside of a project, the script is run in the " +
|
||||
"context of that project). If it points to a directory, the project " +
|
||||
"from that directory is run. If a PATH is not provided, a project in " +
|
||||
"the current working directory is run."
|
||||
)
|
||||
val additionalArgs = Opts.additionalArguments()
|
||||
(pathOpt, jvmArgs, additionalArgs) mapN {
|
||||
(path, jvmArgs, additionalArgs) => (_: Config) =>
|
||||
val enginesRoot = DistributionManager.paths.engines
|
||||
println(s"Launch runner for $path")
|
||||
println(s"JVM=$jvmArgs, additionalArgs=$additionalArgs")
|
||||
println(s"Engines are located at $enginesRoot")
|
||||
(
|
||||
pathOpt,
|
||||
versionOverride,
|
||||
systemJVMOverride,
|
||||
jvmOpts,
|
||||
additionalArgs
|
||||
) mapN {
|
||||
(path, versionOverride, systemJVMOverride, jvmOpts, additionalArgs) =>
|
||||
(config: Config) =>
|
||||
Launcher(config).runRun(
|
||||
path = path,
|
||||
versionOverride = versionOverride,
|
||||
useSystemJVM = systemJVMOverride,
|
||||
jvmOpts = jvmOpts,
|
||||
additionalArguments = additionalArgs
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -98,19 +135,20 @@ object Main {
|
||||
Opts.optionalParameter[String](
|
||||
"interface",
|
||||
"INTERFACE",
|
||||
"Interface for processing all incoming connections."
|
||||
"Interface for processing all incoming connections. " +
|
||||
"Defaults to `127.0.0.1`."
|
||||
)
|
||||
val rpcPort =
|
||||
Opts.optionalParameter[Int](
|
||||
"rpc-port",
|
||||
"PORT",
|
||||
"RPC port for processing all incoming connections."
|
||||
"RPC port for processing all incoming connections. Defaults to 8080."
|
||||
)
|
||||
val dataPort =
|
||||
Opts.optionalParameter[Int](
|
||||
"data-port",
|
||||
"PORT",
|
||||
"Data port for visualisation protocol."
|
||||
"Data port for visualisation protocol. Defaults to 8081."
|
||||
)
|
||||
val additionalArgs = Opts.additionalArguments()
|
||||
(
|
||||
@ -119,45 +157,80 @@ object Main {
|
||||
interface,
|
||||
rpcPort,
|
||||
dataPort,
|
||||
jvmArgs,
|
||||
versionOverride,
|
||||
systemJVMOverride,
|
||||
jvmOpts,
|
||||
additionalArgs
|
||||
) mapN {
|
||||
(rootId, path, interface, rpcPort, dataPort, jvmArgs, additionalArgs) =>
|
||||
(_: Config) =>
|
||||
println(s"Launch language server in $path with id=$rootId.")
|
||||
println(
|
||||
s"interface=$interface, rpcPort=$rpcPort, dataPort=$dataPort"
|
||||
)
|
||||
println(s"JVM=$jvmArgs, additionalArgs=$additionalArgs")
|
||||
(
|
||||
rootId,
|
||||
path,
|
||||
interface,
|
||||
rpcPort,
|
||||
dataPort,
|
||||
versionOverride,
|
||||
systemJVMOverride,
|
||||
jvmOpts,
|
||||
additionalArgs
|
||||
) => (config: Config) =>
|
||||
Launcher(config).runLanguageServer(
|
||||
options = LanguageServerOptions(
|
||||
rootId = rootId,
|
||||
path = path,
|
||||
interface = interface.getOrElse("127.0.0.1"),
|
||||
rpcPort = rpcPort.getOrElse(8080),
|
||||
dataPort = dataPort.getOrElse(8081)
|
||||
),
|
||||
versionOverride = versionOverride,
|
||||
useSystemJVM = systemJVMOverride,
|
||||
jvmOpts = jvmOpts,
|
||||
additionalArguments = additionalArgs
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private def replCommand: Command[Config => Unit] =
|
||||
Command(
|
||||
"repl",
|
||||
"Launch an Enso REPL." +
|
||||
"Launch an Enso REPL. " +
|
||||
"If `auto-confirm` is set, this will install missing engines or " +
|
||||
"runtimes without asking."
|
||||
) {
|
||||
val path = Opts.optionalParameter[Path]("path", "PATH", "Project path.")
|
||||
val path = Opts.optionalParameter[Path](
|
||||
"path",
|
||||
"PATH",
|
||||
"Specifying this option runs the REPL in context of a project " +
|
||||
"located at the given path. The REPL is also run in context of a " +
|
||||
"project if it is launched from within a directory inside a project."
|
||||
)
|
||||
val additionalArgs = Opts.additionalArguments()
|
||||
(path, jvmArgs, additionalArgs) mapN {
|
||||
(path, jvmArgs, additionalArgs) => (config: Config) =>
|
||||
Launcher(config).runRepl(path, jvmArgs, additionalArgs)
|
||||
(path, versionOverride, systemJVMOverride, jvmOpts, additionalArgs) mapN {
|
||||
(path, versionOverride, systemJVMOverride, jvmOpts, additionalArgs) =>
|
||||
(config: Config) =>
|
||||
Launcher(config).runRepl(
|
||||
projectPath = path,
|
||||
versionOverride = versionOverride,
|
||||
useSystemJVM = systemJVMOverride,
|
||||
jvmOpts = jvmOpts,
|
||||
additionalArguments = additionalArgs
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private def defaultCommand: Command[Config => Unit] =
|
||||
Command("default", "Print or change the default Enso version.") {
|
||||
val version = Opts.optionalArgument[String](
|
||||
val version = Opts.optionalArgument[SemVer](
|
||||
"VERSION",
|
||||
"If provided, sets default version to VERSION. " +
|
||||
"Otherwise, current default is displayed."
|
||||
)
|
||||
version map { version => (_: Config) =>
|
||||
version map { version => (config: Config) =>
|
||||
val launcher = Launcher(config)
|
||||
version match {
|
||||
case Some(version) => println(s"Set version to $version")
|
||||
case None => println("Print current version")
|
||||
case Some(version) =>
|
||||
launcher.setDefaultVersion(version)
|
||||
case None =>
|
||||
launcher.printDefaultVersion()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -181,8 +254,8 @@ object Main {
|
||||
Subcommand("engine") {
|
||||
val version = Opts.optionalArgument[SemVer](
|
||||
"VERSION",
|
||||
"The version to install. If not provided, the latest version is " +
|
||||
"installed."
|
||||
"VERSION specifies the engine version to install. If not provided, the" +
|
||||
"latest version is installed."
|
||||
)
|
||||
version map { version => (config: Config) =>
|
||||
version match {
|
||||
@ -365,19 +438,19 @@ object Main {
|
||||
Launcher.ensurePortable()
|
||||
}
|
||||
|
||||
val globalCLIOptions = GlobalCLIOptions(
|
||||
autoConfirm = autoConfirm,
|
||||
hideProgress = hideProgress
|
||||
)
|
||||
|
||||
if (help) {
|
||||
printTopLevelHelp()
|
||||
TopLevelBehavior.Halt
|
||||
} else if (version) {
|
||||
Launcher.displayVersion(useJSON)
|
||||
Launcher(globalCLIOptions).displayVersion(useJSON)
|
||||
TopLevelBehavior.Halt
|
||||
} else
|
||||
TopLevelBehavior.Continue(
|
||||
GlobalCLIOptions(
|
||||
autoConfirm = autoConfirm,
|
||||
hideProgress = hideProgress
|
||||
)
|
||||
)
|
||||
TopLevelBehavior.Continue(globalCLIOptions)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -54,5 +54,18 @@ case class LauncherUpgradeRequiredError(expectedVersion: SemVer)
|
||||
case class UnrecognizedComponentError(message: String, cause: Throwable = null)
|
||||
extends ComponentsException(message, cause)
|
||||
|
||||
/**
|
||||
* Indicates that the component is installed, but its installation is
|
||||
* corrupted.
|
||||
*
|
||||
* Most common reason for this exception is that some critical files are
|
||||
* missing.
|
||||
*/
|
||||
case class CorruptedComponentError(message: String, cause: Throwable = null)
|
||||
extends ComponentsException(message, cause)
|
||||
|
||||
/**
|
||||
* Indicates the requested component is not installed.
|
||||
*/
|
||||
case class ComponentMissingError(message: String, cause: Throwable = null)
|
||||
extends ComponentsException(message, cause)
|
||||
|
@ -15,39 +15,8 @@ import org.enso.launcher.releases.{
|
||||
}
|
||||
import org.enso.launcher.{FileSystem, Launcher, Logger}
|
||||
|
||||
import scala.util.{Failure, Success, Try}
|
||||
import scala.util.control.NonFatal
|
||||
|
||||
/**
|
||||
* Represents a runtime component.
|
||||
*
|
||||
* @param version version of the component
|
||||
* @param path path to the component
|
||||
*/
|
||||
case class Runtime(version: RuntimeVersion, path: Path) {
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
override def toString: String =
|
||||
s"GraalVM ${version.graal}-java${version.java}"
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents an engine component.
|
||||
*
|
||||
* @param version version of the component
|
||||
* @param path path to the component
|
||||
* @param manifest manifest of the engine release
|
||||
*/
|
||||
case class Engine(version: SemVer, path: Path, manifest: Manifest) {
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
override def toString: String =
|
||||
s"Enso Engine $version"
|
||||
}
|
||||
import scala.util.{Failure, Success, Try}
|
||||
|
||||
/**
|
||||
* Manages runtime and engine components.
|
||||
@ -85,11 +54,24 @@ class ComponentsManager(
|
||||
val name = runtimeNameForVersion(version)
|
||||
val path = distributionManager.paths.runtimes / name
|
||||
if (Files.exists(path)) {
|
||||
// TODO [RW] add a sanity check if runtime is in a working state - check
|
||||
// if it at least has the `java` executable, in #976 do the check and
|
||||
// throw an exception on failure, in #1052 offer to repair the broken
|
||||
// installation
|
||||
// TODO [RW] for now an exception is thrown if the installation is
|
||||
// corrupted, in #1052 offer to repair the broken installation
|
||||
loadGraalRuntime(path)
|
||||
.map(Some(_))
|
||||
.recoverWith {
|
||||
case e: Exception =>
|
||||
Failure(
|
||||
UnrecognizedComponentError(
|
||||
s"The runtime $version is already installed, but cannot be " +
|
||||
s"loaded due to $e. Until the launcher gets an auto-repair " +
|
||||
s"feature, please try reinstalling the runtime by " +
|
||||
s"uninstalling all engines that use it and installing them " +
|
||||
s"again, or manually removing `$path`.",
|
||||
e
|
||||
)
|
||||
)
|
||||
}
|
||||
.get
|
||||
} else None
|
||||
}
|
||||
|
||||
@ -136,7 +118,7 @@ class ComponentsManager(
|
||||
val name = engineNameForVersion(version)
|
||||
val path = distributionManager.paths.engines / name
|
||||
if (Files.exists(path)) {
|
||||
// TODO [RW] right now we throw an exception, in the future (#1052) we
|
||||
// TODO [RW] right now we return an exception, in the future (#1052) we
|
||||
// will try recovery
|
||||
loadEngine(path)
|
||||
} else Failure(ComponentMissingError(s"Engine $version is not installed."))
|
||||
@ -208,30 +190,40 @@ class ComponentsManager(
|
||||
def listInstalledRuntimes(): Seq[Runtime] =
|
||||
FileSystem
|
||||
.listDirectory(distributionManager.paths.runtimes)
|
||||
.flatMap(loadGraalRuntime)
|
||||
.map(path => (path, loadGraalRuntime(path)))
|
||||
.flatMap(handleErrorsAsWarnings[Runtime]("A runtime"))
|
||||
|
||||
/**
|
||||
* Lists all installed engines.
|
||||
* @return
|
||||
*/
|
||||
def listInstalledEngines(): Seq[Engine] = {
|
||||
def handleErrorsAsWarnings(path: Path, result: Try[Engine]): Seq[Engine] =
|
||||
result match {
|
||||
case Failure(exception) =>
|
||||
Logger.warn(
|
||||
s"An engine at $path has been skipped due to the " +
|
||||
s"following error: $exception"
|
||||
)
|
||||
Seq()
|
||||
case Success(value) => Seq(value)
|
||||
}
|
||||
|
||||
FileSystem
|
||||
.listDirectory(distributionManager.paths.engines)
|
||||
.map(path => (path, loadEngine(path)))
|
||||
.flatMap((handleErrorsAsWarnings _).tupled)
|
||||
|
||||
.flatMap(handleErrorsAsWarnings[Engine]("An engine"))
|
||||
}
|
||||
|
||||
/**
|
||||
* A helper function that is used when listing components.
|
||||
*
|
||||
* A component error is non-fatal in context of listing, so it is issued as a
|
||||
* warning and the component is treated as non-existent in the list.
|
||||
*/
|
||||
private def handleErrorsAsWarnings[A](name: String)(
|
||||
result: (Path, Try[A])
|
||||
): Seq[A] =
|
||||
result match {
|
||||
case (path, Failure(exception)) =>
|
||||
Logger.warn(
|
||||
s"$name at $path has been skipped due to the following error: " +
|
||||
s"$exception"
|
||||
)
|
||||
Seq()
|
||||
case (_, Success(value)) => Seq(value)
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the latest released version of the engine, by asking the
|
||||
* [[engineReleaseProvider]].
|
||||
@ -269,7 +261,7 @@ class ComponentsManager(
|
||||
FileSystem.withTemporaryDirectory("enso-install") { directory =>
|
||||
Logger.debug(s"Downloading packages to $directory")
|
||||
val enginePackage = directory / engineRelease.packageFileName
|
||||
Logger.info(s"Downloading ${enginePackage.getFileName}")
|
||||
Logger.info(s"Downloading ${enginePackage.getFileName}.")
|
||||
engineReleaseProvider
|
||||
.downloadPackage(engineRelease, enginePackage)
|
||||
.waitForResult(showProgress)
|
||||
@ -278,7 +270,7 @@ class ComponentsManager(
|
||||
val engineDirectoryName =
|
||||
engineDirectoryNameForVersion(engineRelease.version)
|
||||
|
||||
Logger.info(s"Extracting engine")
|
||||
Logger.info(s"Extracting the engine.")
|
||||
Archive
|
||||
.extractArchive(
|
||||
enginePackage,
|
||||
@ -353,14 +345,25 @@ class ComponentsManager(
|
||||
|
||||
/**
|
||||
* Loads the GraalVM runtime definition.
|
||||
*
|
||||
* Returns None on failure.
|
||||
*/
|
||||
private def loadGraalRuntime(path: Path): Option[Runtime] = {
|
||||
private def loadGraalRuntime(path: Path): Try[Runtime] = {
|
||||
val name = path.getFileName.toString
|
||||
def verifyRuntime(runtime: Runtime): Try[Unit] =
|
||||
if (runtime.isValid) {
|
||||
Success(())
|
||||
} else {
|
||||
Failure(CorruptedComponentError(s"Runtime $runtime is corrupted."))
|
||||
}
|
||||
|
||||
for {
|
||||
version <- parseGraalRuntimeVersionString(name)
|
||||
} yield Runtime(version, path)
|
||||
.toRight(
|
||||
UnrecognizedComponentError(s"Invalid runtime component name `$name`.")
|
||||
)
|
||||
.toTry
|
||||
runtime = Runtime(version, path)
|
||||
_ <- verifyRuntime(runtime)
|
||||
} yield runtime
|
||||
}
|
||||
|
||||
/**
|
||||
@ -391,14 +394,24 @@ class ComponentsManager(
|
||||
|
||||
/**
|
||||
* Loads the engine definition.
|
||||
*
|
||||
* Returns None on failure.
|
||||
*/
|
||||
private def loadEngine(path: Path): Try[Engine] =
|
||||
private def loadEngine(path: Path): Try[Engine] = {
|
||||
def verifyEngine(engine: Engine): Try[Unit] =
|
||||
if (!engine.isValid) {
|
||||
Failure(
|
||||
CorruptedComponentError(s"Engine ${engine.version} is corrupted.")
|
||||
)
|
||||
} else {
|
||||
Success(())
|
||||
}
|
||||
|
||||
for {
|
||||
version <- parseEngineVersion(path)
|
||||
manifest <- loadAndCheckEngineManifest(path)
|
||||
} yield Engine(version, path, manifest)
|
||||
engine = Engine(version, path, manifest)
|
||||
_ <- verifyEngine(engine)
|
||||
} yield engine
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the engine version from its path.
|
||||
@ -441,7 +454,7 @@ class ComponentsManager(
|
||||
FileSystem.withTemporaryDirectory("enso-install-runtime") { directory =>
|
||||
val runtimePackage =
|
||||
directory / runtimeReleaseProvider.packageFileName(runtimeVersion)
|
||||
Logger.info(s"Downloading ${runtimePackage.getFileName}")
|
||||
Logger.info(s"Downloading ${runtimePackage.getFileName}.")
|
||||
runtimeReleaseProvider
|
||||
.downloadPackage(runtimeVersion, runtimePackage)
|
||||
.waitForResult(showProgress)
|
||||
@ -449,7 +462,7 @@ class ComponentsManager(
|
||||
|
||||
val runtimeDirectoryName = graalDirectoryForVersion(runtimeVersion)
|
||||
|
||||
Logger.info(s"Extracting runtime")
|
||||
Logger.info(s"Extracting the runtime.")
|
||||
Archive
|
||||
.extractArchive(
|
||||
runtimePackage,
|
||||
@ -470,7 +483,7 @@ class ComponentsManager(
|
||||
|
||||
try {
|
||||
val temporaryRuntime = loadGraalRuntime(runtimeTemporaryPath)
|
||||
if (temporaryRuntime.isEmpty) {
|
||||
if (temporaryRuntime.isFailure) {
|
||||
throw InstallationError(
|
||||
"Cannot load the installed runtime. The package may have been " +
|
||||
"corrupted. Reverting installation."
|
||||
|
@ -0,0 +1,49 @@
|
||||
package org.enso.launcher.components
|
||||
|
||||
import java.nio.file.{Files, Path}
|
||||
|
||||
import nl.gn0s1s.bump.SemVer
|
||||
import org.enso.launcher.FileSystem.PathSyntax
|
||||
|
||||
/**
|
||||
* Represents an engine component.
|
||||
*
|
||||
* @param version version of the component
|
||||
* @param path path to the component
|
||||
* @param manifest manifest of the engine release
|
||||
*/
|
||||
case class Engine(version: SemVer, path: Path, manifest: Manifest) {
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
override def toString: String =
|
||||
s"Enso Engine $version"
|
||||
|
||||
/**
|
||||
* The runtime version that is associated with this engine and should be used
|
||||
* for running it.
|
||||
*/
|
||||
def graalRuntimeVersion: RuntimeVersion = manifest.runtimeVersion
|
||||
|
||||
/**
|
||||
* A set of JVM options that should be added when running this engine.
|
||||
*/
|
||||
def defaultJVMOptions: Seq[Manifest.JVMOption] = manifest.jvmOptions
|
||||
|
||||
/**
|
||||
* Path to the runner JAR.
|
||||
*/
|
||||
def runnerPath: Path = path / "component" / "runner.jar"
|
||||
|
||||
/**
|
||||
* Path to the runtime JAR.
|
||||
*/
|
||||
def runtimePath: Path = path / "component" / "runtime.jar"
|
||||
|
||||
/**
|
||||
* Returns if the installation seems not-corrupted.
|
||||
*/
|
||||
def isValid: Boolean =
|
||||
Files.exists(runnerPath) && Files.exists(runtimePath)
|
||||
}
|
@ -3,26 +3,15 @@ package org.enso.launcher.components
|
||||
import java.io.FileReader
|
||||
import java.nio.file.Path
|
||||
|
||||
import io.circe.{yaml, Decoder, DecodingFailure}
|
||||
import cats.Show
|
||||
import io.circe.{yaml, Decoder}
|
||||
import nl.gn0s1s.bump.SemVer
|
||||
import org.enso.launcher.OS
|
||||
import org.enso.launcher.components.Manifest.JVMOption
|
||||
import org.enso.pkg.SemVerDecoder._
|
||||
|
||||
import scala.util.{Failure, Try, Using}
|
||||
|
||||
/**
|
||||
* Version information identifying the runtime that can be used with an engine
|
||||
* release.
|
||||
*
|
||||
* @param graal version of the GraalVM
|
||||
* @param java Java version of the GraalVM flavour that should be used
|
||||
*/
|
||||
case class RuntimeVersion(graal: SemVer, java: String) {
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
override def toString: String = s"GraalVM $graal Java $java"
|
||||
}
|
||||
|
||||
/**
|
||||
* Contains release metadata read from the manifest file that is attached to
|
||||
* each release.
|
||||
@ -35,11 +24,14 @@ case class RuntimeVersion(graal: SemVer, java: String) {
|
||||
* @param graalVMVersion the version of the GraalVM runtime that has to be
|
||||
* used with this engine
|
||||
* @param graalJavaVersion the java version of that GraalVM runtime
|
||||
* @param jvmOptions a list of JVM options that should be added when running
|
||||
* this engine
|
||||
*/
|
||||
case class Manifest(
|
||||
minimumLauncherVersion: SemVer,
|
||||
graalVMVersion: SemVer,
|
||||
graalJavaVersion: String
|
||||
graalJavaVersion: String,
|
||||
jvmOptions: Seq[JVMOption]
|
||||
) {
|
||||
|
||||
/**
|
||||
@ -57,6 +49,66 @@ object Manifest {
|
||||
*/
|
||||
val DEFAULT_MANIFEST_NAME = "manifest.yaml"
|
||||
|
||||
/**
|
||||
* Context used to substitute context-dependent variables in an JVM option.
|
||||
*
|
||||
* The context depends on what engine is being run on the JVM.
|
||||
*
|
||||
* @param enginePackagePath absolute path to the engine that is being
|
||||
* launched
|
||||
*/
|
||||
case class JVMOptionsContext(enginePackagePath: Path)
|
||||
|
||||
/**
|
||||
* Represents an option that is added to the JVM running an engine.
|
||||
*
|
||||
* @param value option value, possibly containing variables that will be
|
||||
* substituted
|
||||
* @param osRestriction the option is added only on the specified operating
|
||||
* system, if it is None, it applies to all systems
|
||||
*/
|
||||
case class JVMOption(value: String, osRestriction: Option[OS]) {
|
||||
|
||||
/**
|
||||
* Checks if the option applies on the operating system that is currently
|
||||
* running.
|
||||
*/
|
||||
def isRelevant: Boolean =
|
||||
osRestriction.isEmpty || osRestriction.contains(OS.operatingSystem)
|
||||
|
||||
/**
|
||||
* Substitutes any variables based on the provided `context`.
|
||||
*/
|
||||
def substitute(context: JVMOptionsContext): String =
|
||||
value.replace(
|
||||
"$enginePackagePath",
|
||||
context.enginePackagePath.toAbsolutePath.normalize.toString
|
||||
)
|
||||
}
|
||||
|
||||
object JVMOption {
|
||||
private object Fields {
|
||||
val os = "os"
|
||||
val value = "value"
|
||||
}
|
||||
|
||||
/**
|
||||
* [[Decoder]] instance that allows to parse the [[JVMOption]] from the
|
||||
* YAML manifest.
|
||||
*/
|
||||
implicit val decoder: Decoder[JVMOption] = { json =>
|
||||
val hasOSKey = json.keys.exists { keyList: Iterable[String] =>
|
||||
keyList.toSeq.contains(Fields.os)
|
||||
}
|
||||
|
||||
for {
|
||||
value <- json.get[String](Fields.value)
|
||||
osRestriction <-
|
||||
if (hasOSKey) json.get[OS](Fields.os).map(Some(_)) else Right(None)
|
||||
} yield JVMOption(value, osRestriction)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to load the manifest at the given path.
|
||||
*
|
||||
@ -68,8 +120,9 @@ object Manifest {
|
||||
.parse(reader)
|
||||
.flatMap(_.as[Manifest])
|
||||
.toTry
|
||||
.recoverWith { error => Failure(ManifestLoadingError(error)) }
|
||||
}.flatten
|
||||
}.flatten.recoverWith { error =>
|
||||
Failure(ManifestLoadingError.fromThrowable(error))
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the manifest from a string containing a YAML definition.
|
||||
@ -81,30 +134,52 @@ object Manifest {
|
||||
.parse(yamlString)
|
||||
.flatMap(_.as[Manifest])
|
||||
.toTry
|
||||
.recoverWith { error => Failure(ManifestLoadingError(error)) }
|
||||
.recoverWith { error =>
|
||||
Failure(ManifestLoadingError.fromThrowable(error))
|
||||
}
|
||||
}
|
||||
|
||||
case class ManifestLoadingError(cause: Throwable)
|
||||
extends RuntimeException(s"Could not load the manifest: $cause", cause)
|
||||
/**
|
||||
* Indicates an error that prevented loading the engine manifest.
|
||||
*/
|
||||
case class ManifestLoadingError(message: String, cause: Throwable)
|
||||
extends RuntimeException(message, cause) {
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
override def toString: String = message
|
||||
}
|
||||
|
||||
object ManifestLoadingError {
|
||||
|
||||
/**
|
||||
* Creates a [[ManifestLoadingError]] 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): ManifestLoadingError =
|
||||
throwable match {
|
||||
case decodingError: io.circe.Error =>
|
||||
val errorMessage =
|
||||
implicitly[Show[io.circe.Error]].show(decodingError)
|
||||
ManifestLoadingError(
|
||||
s"Could not parse the manifest: $errorMessage",
|
||||
decodingError
|
||||
)
|
||||
case other =>
|
||||
ManifestLoadingError(s"Could not load the manifest: $other", other)
|
||||
}
|
||||
}
|
||||
|
||||
private object Fields {
|
||||
val minimumLauncherVersion = "minimum-launcher-version"
|
||||
val jvmOptions = "jvm-options"
|
||||
val graalVMVersion = "graal-vm-version"
|
||||
val graalJavaVersion = "graal-java-version"
|
||||
}
|
||||
|
||||
implicit private val semverDecoder: Decoder[SemVer] = { json =>
|
||||
for {
|
||||
string <- json.as[String]
|
||||
version <- SemVer(string).toRight(
|
||||
DecodingFailure(
|
||||
s"`$string` is not a valid semver version.",
|
||||
json.history
|
||||
)
|
||||
)
|
||||
} yield version
|
||||
}
|
||||
|
||||
implicit private val decoder: Decoder[Manifest] = { json =>
|
||||
for {
|
||||
minimumLauncherVersion <- json.get[SemVer](Fields.minimumLauncherVersion)
|
||||
@ -113,10 +188,12 @@ object Manifest {
|
||||
json
|
||||
.get[String](Fields.graalJavaVersion)
|
||||
.orElse(json.get[Int](Fields.graalJavaVersion).map(_.toString))
|
||||
jvmOptions <- json.getOrElse[Seq[JVMOption]](Fields.jvmOptions)(Seq())
|
||||
} yield Manifest(
|
||||
minimumLauncherVersion = minimumLauncherVersion,
|
||||
graalVMVersion = graalVMVersion,
|
||||
graalJavaVersion = graalJavaVersion
|
||||
graalJavaVersion = graalJavaVersion,
|
||||
jvmOptions = jvmOptions
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,45 @@
|
||||
package org.enso.launcher.components
|
||||
|
||||
import java.nio.file.{Files, Path}
|
||||
|
||||
import org.enso.launcher.FileSystem.PathSyntax
|
||||
import org.enso.launcher.OS
|
||||
|
||||
/**
|
||||
* Represents a runtime component.
|
||||
*
|
||||
* @param version version of the component
|
||||
* @param path path to the component
|
||||
*/
|
||||
case class Runtime(version: RuntimeVersion, path: Path) {
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
override def toString: String =
|
||||
s"GraalVM ${version.graal}-java${version.java}"
|
||||
|
||||
/**
|
||||
* The path to the JAVA_HOME directory associated with this runtime.
|
||||
*/
|
||||
def javaHome: Path =
|
||||
OS.operatingSystem match {
|
||||
case OS.Linux => path
|
||||
case OS.MacOS => path / "Contents" / "Home"
|
||||
case OS.Windows => path
|
||||
}
|
||||
|
||||
/**
|
||||
* The path to the `java` executable associated with this runtime.
|
||||
*/
|
||||
def javaExecutable: Path = {
|
||||
val executableName = if (OS.isWindows) "java.exe" else "java"
|
||||
javaHome / "bin" / executableName
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns if the installation seems not-corrupted.
|
||||
*/
|
||||
def isValid: Boolean =
|
||||
Files.exists(javaExecutable) && Files.isExecutable(javaExecutable)
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
package org.enso.launcher.components
|
||||
|
||||
import nl.gn0s1s.bump.SemVer
|
||||
|
||||
/**
|
||||
* Version information identifying the runtime that can be used with an engine
|
||||
* release.
|
||||
*
|
||||
* @param graal version of the GraalVM
|
||||
* @param java Java version of the GraalVM flavour that should be used
|
||||
*/
|
||||
case class RuntimeVersion(graal: SemVer, java: String) {
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
override def toString: String = s"GraalVM $graal Java $java"
|
||||
}
|
@ -0,0 +1,74 @@
|
||||
package org.enso.launcher.components.runner
|
||||
|
||||
import org.enso.launcher.Logger
|
||||
|
||||
import scala.sys.process.{Process, ProcessBuilder}
|
||||
import scala.util.{Failure, Try}
|
||||
|
||||
/**
|
||||
* Represents information required to run a system command.
|
||||
*
|
||||
* @param command the command and its arguments that should be executed
|
||||
* @param extraEnv environment variables that should be overridden
|
||||
*/
|
||||
case class Command(command: Seq[String], extraEnv: Seq[(String, String)]) {
|
||||
|
||||
/**
|
||||
* Runs the command and returns its exit code.
|
||||
*
|
||||
* May return an exception if it is impossible to run the command (for
|
||||
* example due to insufficient permissions or nonexistent executable).
|
||||
*/
|
||||
def run(): Try[Int] =
|
||||
runProcess { processBuilder =>
|
||||
processBuilder.run(connectInput = true).exitValue()
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the command and returns its standard output as [[String]].
|
||||
*
|
||||
* The standard error is printed to the console.
|
||||
*
|
||||
* May return an exception if it is impossible to run the command or the
|
||||
* command returned non-zero exit code.
|
||||
*/
|
||||
def captureOutput(): Try[String] =
|
||||
runProcess { processBuilder =>
|
||||
processBuilder.!!
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares to run a process and invokes `action` on the created
|
||||
* [[ProcessBuilder]], capturing any errors.
|
||||
*
|
||||
* On success, the result of `action` is returned.
|
||||
*/
|
||||
private def runProcess[R](action: ProcessBuilder => R): Try[R] = {
|
||||
val result = Try {
|
||||
Logger.debug(s"Executing $toString")
|
||||
val processBuilder = Process(command, None, extraEnv: _*)
|
||||
action(processBuilder)
|
||||
}
|
||||
result.recoverWith(error =>
|
||||
Failure(
|
||||
RunnerError(
|
||||
s"Could not run the command $toString due to: $error",
|
||||
error
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* A textual representation of the command in a format that can be copied in
|
||||
* to a terminal and executed.
|
||||
*/
|
||||
override def toString: String = {
|
||||
def escapeQuotes(string: String): String =
|
||||
"\"" + string.replace("\"", "\\\"") + "\""
|
||||
val environmentDescription =
|
||||
extraEnv.map(v => s"${v._1}=${escapeQuotes(v._2)} ").mkString
|
||||
val commandDescription = command.map(escapeQuotes).mkString(" ")
|
||||
environmentDescription + commandDescription
|
||||
}
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
package org.enso.launcher.components.runner
|
||||
|
||||
/**
|
||||
* Represents settings that are used to launch the runtime JVM.
|
||||
*
|
||||
* @param useSystemJVM if set, the system configured JVM is used instead of
|
||||
* the one managed by the launcher
|
||||
* @param jvmOptions options that should be added to the launched JVM
|
||||
*/
|
||||
case class JVMSettings(useSystemJVM: Boolean, jvmOptions: Seq[(String, String)])
|
@ -0,0 +1,21 @@
|
||||
package org.enso.launcher.components.runner
|
||||
|
||||
import java.nio.file.Path
|
||||
import java.util.UUID
|
||||
|
||||
/**
|
||||
* Options that are passed to the language server.
|
||||
*
|
||||
* @param rootId an id of content root
|
||||
* @param path a path to the content root
|
||||
* @param interface a interface that the server listen to
|
||||
* @param rpcPort an RPC port that the server listen to
|
||||
* @param dataPort a data port that the server listen to
|
||||
*/
|
||||
case class LanguageServerOptions(
|
||||
rootId: UUID,
|
||||
path: Path,
|
||||
interface: String,
|
||||
rpcPort: Int,
|
||||
dataPort: Int
|
||||
)
|
@ -0,0 +1,11 @@
|
||||
package org.enso.launcher.components.runner
|
||||
|
||||
import nl.gn0s1s.bump.SemVer
|
||||
|
||||
/**
|
||||
* Represents settings that are used to launch the runner JAR.
|
||||
*
|
||||
* @param version Enso engine version to use
|
||||
* @param runnerArguments arguments that should be passed to the runner
|
||||
*/
|
||||
case class RunSettings(version: SemVer, runnerArguments: Seq[String])
|
@ -0,0 +1,266 @@
|
||||
package org.enso.launcher.components.runner
|
||||
|
||||
import java.nio.file.{Files, Path}
|
||||
|
||||
import nl.gn0s1s.bump.SemVer
|
||||
import org.enso.launcher.components.{ComponentsManager, Manifest, Runtime}
|
||||
import org.enso.launcher.project.ProjectManager
|
||||
import org.enso.launcher.{Environment, GlobalConfigurationManager, Logger}
|
||||
|
||||
import scala.util.Try
|
||||
|
||||
/**
|
||||
* A helper class that prepares settings for running Enso components and
|
||||
* converts these settings to actual commands that launch the component inside
|
||||
* of a JVM.
|
||||
*/
|
||||
class Runner(
|
||||
projectManager: ProjectManager,
|
||||
configurationManager: GlobalConfigurationManager,
|
||||
componentsManager: ComponentsManager,
|
||||
environment: Environment
|
||||
) {
|
||||
|
||||
/**
|
||||
* The current working directory that is a starting point when checking if
|
||||
* the command is launched inside of a project.
|
||||
*
|
||||
* Can be overridden in tests.
|
||||
*/
|
||||
protected val currentWorkingDirectory: Path = Path.of(".")
|
||||
|
||||
/**
|
||||
* Creates [[RunSettings]] for launching the REPL.
|
||||
*
|
||||
* See [[org.enso.launcher.Launcher.runRepl]] for more details.
|
||||
*/
|
||||
def repl(
|
||||
projectPath: Option[Path],
|
||||
versionOverride: Option[SemVer],
|
||||
additionalArguments: Seq[String]
|
||||
): Try[RunSettings] =
|
||||
Try {
|
||||
val inProject = projectPath match {
|
||||
case Some(value) =>
|
||||
Some(projectManager.loadProject(value).get)
|
||||
case None =>
|
||||
projectManager.findProject(currentWorkingDirectory).get
|
||||
}
|
||||
|
||||
val version =
|
||||
versionOverride.getOrElse {
|
||||
inProject
|
||||
.map(_.version)
|
||||
.getOrElse(configurationManager.defaultVersion)
|
||||
}
|
||||
val arguments = inProject match {
|
||||
case Some(project) =>
|
||||
val projectPackagePath =
|
||||
project.path.toAbsolutePath.normalize.toString
|
||||
Seq("--repl", "--in-project", projectPackagePath)
|
||||
case None =>
|
||||
Seq("--repl")
|
||||
}
|
||||
RunSettings(version, arguments ++ additionalArguments)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates [[RunSettings]] for running Enso projects or scripts.
|
||||
*
|
||||
* See [[org.enso.launcher.Launcher.runRun]] for more details.
|
||||
*/
|
||||
def run(
|
||||
path: Option[Path],
|
||||
versionOverride: Option[SemVer],
|
||||
additionalArguments: Seq[String]
|
||||
): Try[RunSettings] =
|
||||
Try {
|
||||
val actualPath = path
|
||||
.getOrElse {
|
||||
projectManager
|
||||
.findProject(currentWorkingDirectory)
|
||||
.get
|
||||
.getOrElse {
|
||||
throw RunnerError(
|
||||
"The current directory is not inside any project. `enso run` " +
|
||||
"should either get a path to a project or script to run, or " +
|
||||
"be run inside of a project to run that project."
|
||||
)
|
||||
}
|
||||
.path
|
||||
}
|
||||
.toAbsolutePath
|
||||
.normalize()
|
||||
if (!Files.exists(actualPath)) {
|
||||
throw RunnerError(s"$actualPath does not exist")
|
||||
}
|
||||
val projectMode = Files.isDirectory(actualPath)
|
||||
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 arguments =
|
||||
if (projectMode) Seq("--run", actualPath.toString)
|
||||
else
|
||||
project match {
|
||||
case Some(project) =>
|
||||
Seq(
|
||||
"--run",
|
||||
actualPath.toString,
|
||||
"--in-project",
|
||||
project.path.toAbsolutePath.normalize().toString
|
||||
)
|
||||
case None =>
|
||||
Seq("--run", actualPath.toString)
|
||||
}
|
||||
RunSettings(version, arguments ++ additionalArguments)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates [[RunSettings]] for launching the Language Server.
|
||||
*
|
||||
* See [[org.enso.launcher.Launcher.runLanguageServer]] for more details.
|
||||
*/
|
||||
def languageServer(
|
||||
options: LanguageServerOptions,
|
||||
versionOverride: Option[SemVer],
|
||||
additionalArguments: Seq[String]
|
||||
): Try[RunSettings] =
|
||||
Try {
|
||||
val project = projectManager.loadProject(options.path).get
|
||||
val version = versionOverride.getOrElse(project.version)
|
||||
val arguments = Seq(
|
||||
"--server",
|
||||
"--root-id",
|
||||
options.rootId.toString,
|
||||
"--path",
|
||||
options.path.toAbsolutePath.normalize.toString,
|
||||
"--interface",
|
||||
options.interface,
|
||||
"--rpc-port",
|
||||
options.rpcPort.toString,
|
||||
"--data-port",
|
||||
options.dataPort.toString
|
||||
)
|
||||
RunSettings(version, arguments ++ additionalArguments)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates [[RunSettings]] for querying the currently selected engine
|
||||
* version.
|
||||
*
|
||||
* If the current working directory is inside of a project, the engine
|
||||
* associated with the project is queried, otherwise the default engine is
|
||||
* queried.
|
||||
*
|
||||
* @param useJSON if set to true, the returned [[RunSettings]] will request
|
||||
* the version in JSON format, otherwise human readable text
|
||||
* format will be used
|
||||
* @return the [[RunSettings]] and a [[WhichEngine]] indicating if the used
|
||||
* engine was from a project (true) or the default one (false)
|
||||
*/
|
||||
def version(useJSON: Boolean): Try[(RunSettings, WhichEngine)] = {
|
||||
for {
|
||||
project <- projectManager.findProject(currentWorkingDirectory)
|
||||
} yield {
|
||||
val version =
|
||||
project.map(_.version).getOrElse(configurationManager.defaultVersion)
|
||||
val arguments =
|
||||
Seq("--version") ++ (if (useJSON) Seq("--json") else Seq())
|
||||
|
||||
val whichEngine =
|
||||
project match {
|
||||
case Some(value) => WhichEngine.FromProject(value.name)
|
||||
case None => WhichEngine.Default
|
||||
}
|
||||
|
||||
(RunSettings(version, arguments), whichEngine)
|
||||
}
|
||||
}
|
||||
|
||||
final private val JVM_OPTIONS_ENV_VAR = "ENSO_JVM_OPTS"
|
||||
|
||||
/**
|
||||
* Creates a command that can be used to launch the component.
|
||||
*
|
||||
* Combines the [[RunSettings]] for the runner with the [[JVMSettings]] for
|
||||
* the underlying JVM to get the full command for launching the component.
|
||||
*/
|
||||
def createCommand(
|
||||
runSettings: RunSettings,
|
||||
jvmSettings: JVMSettings
|
||||
): Command = {
|
||||
val engine = componentsManager.findOrInstallEngine(runSettings.version)
|
||||
val javaCommand =
|
||||
if (jvmSettings.useSystemJVM) systemJavaCommand
|
||||
else {
|
||||
val runtime = componentsManager.findOrInstallRuntime(engine)
|
||||
javaCommandForRuntime(runtime)
|
||||
}
|
||||
|
||||
val jvmOptsFromEnvironment = environment.getEnvVar(JVM_OPTIONS_ENV_VAR)
|
||||
jvmOptsFromEnvironment.foreach { opts =>
|
||||
Logger.debug(
|
||||
s"Picking up additional JVM options ($opts) from the " +
|
||||
s"$JVM_OPTIONS_ENV_VAR environment variable."
|
||||
)
|
||||
}
|
||||
|
||||
val runnerJar = engine.runnerPath.toAbsolutePath.normalize.toString
|
||||
|
||||
def translateJVMOption(option: (String, String)): String = {
|
||||
val name = option._1
|
||||
val value = option._2
|
||||
s"-D$name=$value"
|
||||
}
|
||||
|
||||
val context = Manifest.JVMOptionsContext(enginePackagePath = engine.path)
|
||||
|
||||
val manifestOptions =
|
||||
engine.defaultJVMOptions.filter(_.isRelevant).map(_.substitute(context))
|
||||
val environmentOptions =
|
||||
jvmOptsFromEnvironment.map(_.split(' ').toIndexedSeq).getOrElse(Seq())
|
||||
val commandLineOptions = jvmSettings.jvmOptions.map(translateJVMOption)
|
||||
|
||||
val jvmArguments =
|
||||
manifestOptions ++ environmentOptions ++ commandLineOptions ++
|
||||
Seq("-jar", runnerJar)
|
||||
|
||||
val command = Seq(javaCommand.executableName) ++
|
||||
jvmArguments ++ runSettings.runnerArguments
|
||||
|
||||
val extraEnvironmentOverrides =
|
||||
javaCommand.javaHomeOverride.map("JAVA_HOME" -> _).toSeq
|
||||
|
||||
Command(command, extraEnvironmentOverrides)
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a way of launching the JVM.
|
||||
*
|
||||
* Stores the name of the `java` executable to run and a possible JAVA_HOME
|
||||
* environment variable override.
|
||||
*/
|
||||
private case class JavaCommand(
|
||||
executableName: String,
|
||||
javaHomeOverride: Option[String]
|
||||
)
|
||||
|
||||
/**
|
||||
* The [[JavaCommand]] representing the system-configured JVM.
|
||||
*/
|
||||
private def systemJavaCommand: JavaCommand = JavaCommand("java", None)
|
||||
|
||||
/**
|
||||
* The [[JavaCommand]] representing a managed [[Runtime]].
|
||||
*/
|
||||
private def javaCommandForRuntime(runtime: Runtime): JavaCommand =
|
||||
JavaCommand(
|
||||
executableName = runtime.javaExecutable.toAbsolutePath.normalize.toString,
|
||||
javaHomeOverride =
|
||||
Some(runtime.javaHome.toAbsolutePath.normalize.toString)
|
||||
)
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
package org.enso.launcher.components.runner
|
||||
|
||||
/**
|
||||
* Represents an error encountered when running the component.
|
||||
*/
|
||||
case class RunnerError(message: String, cause: Throwable = null)
|
||||
extends RuntimeException(message, cause) {
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
override def toString: String = message
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
package org.enso.launcher.components.runner
|
||||
|
||||
/**
|
||||
* Returned by [[Runner.version]], specifies if the engine that is queried for
|
||||
* version is from a project or the default one.
|
||||
*/
|
||||
sealed trait WhichEngine
|
||||
object WhichEngine {
|
||||
case class FromProject(name: String) extends WhichEngine
|
||||
case object Default extends WhichEngine
|
||||
}
|
@ -284,14 +284,14 @@ class DistributionInstaller(
|
||||
|
||||
if (bundleAction.copy) {
|
||||
for (engine <- engines) {
|
||||
Logger.info(s"Copying bundled Enso engine ${engine.getFileName}")
|
||||
Logger.info(s"Copying bundled Enso engine ${engine.getFileName}.")
|
||||
FileSystem.copyDirectory(
|
||||
engine,
|
||||
enginesDirectory / engine.getFileName
|
||||
)
|
||||
}
|
||||
for (runtime <- runtimes) {
|
||||
Logger.info(s"Copying bundled runtime ${runtime.getFileName}")
|
||||
Logger.info(s"Copying bundled runtime ${runtime.getFileName}.")
|
||||
FileSystem.copyDirectory(
|
||||
runtime,
|
||||
runtimesDirectory / runtime.getFileName
|
||||
|
@ -0,0 +1,45 @@
|
||||
package org.enso.launcher.project
|
||||
|
||||
import java.io.File
|
||||
import java.nio.file.Path
|
||||
|
||||
import nl.gn0s1s.bump.SemVer
|
||||
import org.enso.launcher.GlobalConfigurationManager
|
||||
import org.enso.pkg.{DefaultEnsoVersion, Package, SemVerEnsoVersion}
|
||||
|
||||
/**
|
||||
* Represents an Enso project.
|
||||
*
|
||||
* @param pkg the package associated with the project
|
||||
* @param globalConfigurationManager the configuration manager that is
|
||||
* necessary when the project version is set
|
||||
* to `default`
|
||||
*/
|
||||
class Project(
|
||||
pkg: Package[File],
|
||||
globalConfigurationManager: GlobalConfigurationManager
|
||||
) {
|
||||
|
||||
/**
|
||||
* The Enso engine version associated with the project.
|
||||
*
|
||||
* If the version in the configuration is set to `default`, the locally
|
||||
* default Enso version is used.
|
||||
*/
|
||||
def version: SemVer =
|
||||
pkg.config.ensoVersion match {
|
||||
case DefaultEnsoVersion =>
|
||||
globalConfigurationManager.defaultVersion
|
||||
case SemVerEnsoVersion(version) => version
|
||||
}
|
||||
|
||||
/**
|
||||
* The package name of the project.
|
||||
*/
|
||||
def name: String = pkg.name
|
||||
|
||||
/**
|
||||
* The path to the content root of the project.
|
||||
*/
|
||||
def path: Path = pkg.root.toPath
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
package org.enso.launcher.project
|
||||
|
||||
import java.nio.file.Path
|
||||
|
||||
/**
|
||||
* Indicates that it was impossible to load the project at a specified path.
|
||||
*/
|
||||
case class ProjectLoadingError(path: Path, cause: Throwable)
|
||||
extends RuntimeException(
|
||||
s"Cannot load an Enso project at `$path` due to: $cause",
|
||||
cause
|
||||
) {
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
override def toString: String = getMessage
|
||||
}
|
@ -0,0 +1,77 @@
|
||||
package org.enso.launcher.project
|
||||
|
||||
import java.nio.file.Path
|
||||
|
||||
import nl.gn0s1s.bump.SemVer
|
||||
import org.enso.launcher.{GlobalConfigurationManager, Logger}
|
||||
import org.enso.pkg.{PackageManager, SemVerEnsoVersion}
|
||||
|
||||
import scala.util.{Failure, Try}
|
||||
|
||||
/**
|
||||
* A helper class for project management.
|
||||
*
|
||||
* 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) {
|
||||
|
||||
private val packageManager = PackageManager.Default
|
||||
|
||||
/**
|
||||
* Creates a new project at the specified path with the given name.
|
||||
*
|
||||
* If the version is not provided, the default Enso engine version is used.
|
||||
*
|
||||
* @param name specifies the name of the project
|
||||
* @param path specifies where the project should be created
|
||||
* @param ensoVersion if provided, specifies an exact Enso version that the
|
||||
* project should be associated with
|
||||
*/
|
||||
def newProject(
|
||||
name: String,
|
||||
path: Path,
|
||||
ensoVersion: Option[SemVer] = None
|
||||
): Unit = {
|
||||
packageManager.create(
|
||||
root = path.toFile,
|
||||
name = name,
|
||||
ensoVersion = SemVerEnsoVersion(
|
||||
ensoVersion.getOrElse(globalConfigurationManager.defaultVersion)
|
||||
)
|
||||
)
|
||||
Logger.info(s"Project created in `$path`.")
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to load the project at the provided `path`.
|
||||
*/
|
||||
def loadProject(path: Path): Try[Project] =
|
||||
packageManager
|
||||
.loadPackage(path.toFile)
|
||||
.map(new Project(_, globalConfigurationManager))
|
||||
.recoverWith(error => Failure(ProjectLoadingError(path, error)))
|
||||
|
||||
/**
|
||||
* Traverses the directory tree looking for a project in one of the ancestors
|
||||
* of the provided `path`.
|
||||
*
|
||||
* If a package file is missing in a directory, its ancestors are searched
|
||||
* recursively. However if a package file exists in some directory, but there
|
||||
* are errors preventing from loading it, that error is reported.
|
||||
*/
|
||||
def findProject(path: Path): Try[Option[Project]] =
|
||||
tryFindingProject(path.toAbsolutePath.normalize).map(Some(_)).recover {
|
||||
case PackageManager.PackageNotFound() => None
|
||||
}
|
||||
|
||||
private def tryFindingProject(root: Path): Try[Project] =
|
||||
packageManager
|
||||
.loadPackage(root.toFile)
|
||||
.map(new Project(_, globalConfigurationManager))
|
||||
.recoverWith {
|
||||
case PackageManager.PackageNotFound() if root.getParent != null =>
|
||||
tryFindingProject(root.getParent)
|
||||
case otherError => Failure(otherError)
|
||||
}
|
||||
}
|
@ -0,0 +1,2 @@
|
||||
placeholder
|
||||
|
@ -0,0 +1,2 @@
|
||||
placeholder
|
||||
|
@ -1,3 +0,0 @@
|
||||
minimum-launcher-version: 0.0.1
|
||||
graal-vm-version: 1.0.0
|
||||
graal-java-version: 11
|
@ -0,0 +1,2 @@
|
||||
placeholder
|
||||
|
@ -0,0 +1,2 @@
|
||||
placeholder
|
||||
|
@ -1,3 +0,0 @@
|
||||
minimum-launcher-version: 0.0.1
|
||||
graal-vm-version: 1.0.0
|
||||
graal-java-version: 11
|
@ -0,0 +1,2 @@
|
||||
placeholder
|
||||
|
@ -0,0 +1,2 @@
|
||||
placeholder
|
||||
|
@ -1,3 +0,0 @@
|
||||
minimum-launcher-version: 0.0.1
|
||||
graal-vm-version: 1.0.0
|
||||
graal-java-version: 11
|
@ -1,3 +1,12 @@
|
||||
minimum-launcher-version: 0.0.1
|
||||
graal-vm-version: 1.0.0
|
||||
graal-java-version: 11
|
||||
jvm-options:
|
||||
- value: "-Dtruffle.class.path.append=$enginePackagePath\\component\\runtime.jar"
|
||||
os: "windows"
|
||||
- value: "-Dtruffle.class.path.append=$enginePackagePath/component/runtime.jar"
|
||||
os: "linux"
|
||||
- value: "-Dtruffle.class.path.append=$enginePackagePath/component/runtime.jar"
|
||||
os: "macos"
|
||||
- value: "-Doptions-added-from-manifest=42"
|
||||
- value: "-Xanother-one"
|
||||
|
@ -0,0 +1,2 @@
|
||||
placeholder
|
||||
|
@ -0,0 +1,2 @@
|
||||
placeholder
|
||||
|
@ -1,3 +0,0 @@
|
||||
minimum-launcher-version: 0.0.1
|
||||
graal-vm-version: 2.0.0
|
||||
graal-java-version: 11
|
@ -0,0 +1,2 @@
|
||||
placeholder
|
||||
|
@ -0,0 +1,2 @@
|
||||
placeholder
|
||||
|
@ -1,3 +0,0 @@
|
||||
minimum-launcher-version: 0.0.1
|
||||
graal-vm-version: 2.0.0
|
||||
graal-java-version: 11
|
@ -0,0 +1,2 @@
|
||||
placeholder
|
||||
|
@ -0,0 +1,2 @@
|
||||
placeholder
|
||||
|
@ -1,3 +0,0 @@
|
||||
minimum-launcher-version: 0.0.1
|
||||
graal-vm-version: 2.0.0
|
||||
graal-java-version: 11
|
@ -0,0 +1,2 @@
|
||||
placeholder
|
||||
|
@ -0,0 +1,2 @@
|
||||
placeholder
|
||||
|
@ -1,3 +0,0 @@
|
||||
minimum-launcher-version: 0.0.1
|
||||
graal-vm-version: 2.0.0
|
||||
graal-java-version: 11
|
@ -0,0 +1,2 @@
|
||||
placeholder
|
||||
|
@ -0,0 +1,2 @@
|
||||
placeholder
|
||||
|
@ -1,3 +0,0 @@
|
||||
minimum-launcher-version: 0.0.1
|
||||
graal-vm-version: 2.0.0
|
||||
graal-java-version: 11
|
@ -0,0 +1,2 @@
|
||||
placeholder
|
||||
|
@ -0,0 +1,2 @@
|
||||
placeholder
|
||||
|
@ -1,3 +0,0 @@
|
||||
minimum-launcher-version: 0.0.1
|
||||
graal-vm-version: 2.0.0
|
||||
graal-java-version: 11
|
@ -29,22 +29,28 @@ trait FakeEnvironment { self: WithTemporaryDirectory =>
|
||||
/**
|
||||
* Returns an [[Environment]] instance that overrides the `ENSO_*`
|
||||
* directories to be inside the temporary directory for the test.
|
||||
*
|
||||
* Additionall environment overrides may be passed that will also be added to
|
||||
* the environment. Note, however, that the `ENSO_*` directories that are
|
||||
* defined in this function take precedence over whatever is passed to
|
||||
* `extraOverrides`.
|
||||
*/
|
||||
def fakeInstalledEnvironment(): Environment = {
|
||||
def fakeInstalledEnvironment(
|
||||
extraOverrides: Map[String, String] = Map.empty
|
||||
): Environment = {
|
||||
val executable = fakeExecutablePath()
|
||||
val dataDir = getTestDirectory / "test_data"
|
||||
val configDir = getTestDirectory / "test_config"
|
||||
val binDir = getTestDirectory / "test_bin"
|
||||
val env = extraOverrides
|
||||
.updated("ENSO_DATA_DIRECTORY", dataDir.toString)
|
||||
.updated("ENSO_CONFIG_DIRECTORY", configDir.toString)
|
||||
.updated("ENSO_BIN_DIRECTORY", binDir.toString)
|
||||
val fakeEnvironment = new Environment {
|
||||
override def getPathToRunningExecutable: Path = executable
|
||||
|
||||
override def getEnvVar(key: String): Option[String] =
|
||||
key match {
|
||||
case "ENSO_DATA_DIRECTORY" => Some(dataDir.toString)
|
||||
case "ENSO_CONFIG_DIRECTORY" => Some(configDir.toString)
|
||||
case "ENSO_BIN_DIRECTORY" => Some(binDir.toString)
|
||||
case _ => super.getEnvVar(key)
|
||||
}
|
||||
env.orElse(Function.unlift(super.getEnvVar)).lift(key)
|
||||
}
|
||||
|
||||
fakeEnvironment
|
||||
|
@ -1,22 +0,0 @@
|
||||
package org.enso.launcher
|
||||
|
||||
import org.scalatest.matchers.should.Matchers
|
||||
import org.scalatest.wordspec.AnyWordSpec
|
||||
|
||||
class LauncherSpec
|
||||
extends AnyWordSpec
|
||||
with Matchers
|
||||
with WithTemporaryDirectory {
|
||||
|
||||
"new command" should {
|
||||
"create a new project with correct structure" in {
|
||||
Logger.suppressWarnings {
|
||||
val projectDir = getTestDirectory.resolve("proj1")
|
||||
Launcher.newProject("TEST", Some(projectDir))
|
||||
projectDir.toFile should exist
|
||||
projectDir.resolve("src").resolve("Main.enso").toFile should exist
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -1,16 +1,22 @@
|
||||
package org.enso.launcher
|
||||
|
||||
import buildinfo.Info
|
||||
import io.circe.parser
|
||||
|
||||
class NativeLauncherSpec extends NativeTest {
|
||||
"native launcher" should {
|
||||
"display its version" in {
|
||||
val run = runLauncher(Seq("--version"))
|
||||
val run = runLauncher(Seq("version", "--json", "--only-launcher"))
|
||||
run should returnSuccess
|
||||
|
||||
run.stdout should include("Enso Launcher")
|
||||
run.stdout should include("Version:")
|
||||
run.stdout should include(Info.ensoVersion)
|
||||
val version = parser.parse(run.stdout).getOrElse {
|
||||
throw new RuntimeException("Version should be a valid JSON string.")
|
||||
}
|
||||
version.asObject.get
|
||||
.apply("version")
|
||||
.get
|
||||
.asString
|
||||
.get shouldEqual Info.ensoVersion
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,53 +1,9 @@
|
||||
package org.enso.launcher.components
|
||||
|
||||
import java.nio.file.Path
|
||||
|
||||
import nl.gn0s1s.bump.SemVer
|
||||
import org.enso.launcher.cli.GlobalCLIOptions
|
||||
import org.enso.launcher.installation.DistributionManager
|
||||
import org.enso.launcher.releases.{
|
||||
EngineReleaseProvider,
|
||||
GraalCEReleaseProvider
|
||||
}
|
||||
import org.enso.launcher.{FakeEnvironment, Logger, WithTemporaryDirectory}
|
||||
import org.scalatest.OptionValues
|
||||
import org.scalatest.matchers.should.Matchers
|
||||
import org.scalatest.wordspec.AnyWordSpec
|
||||
import org.enso.launcher.Logger
|
||||
|
||||
class ComponentsManagerSpec
|
||||
extends AnyWordSpec
|
||||
with Matchers
|
||||
with OptionValues
|
||||
with WithTemporaryDirectory
|
||||
with FakeEnvironment {
|
||||
|
||||
private def makeManagers(): (DistributionManager, ComponentsManager) = {
|
||||
val distributionManager = new DistributionManager(
|
||||
fakeInstalledEnvironment()
|
||||
)
|
||||
val fakeReleasesRoot =
|
||||
Path.of(
|
||||
getClass
|
||||
.getResource("fake-releases")
|
||||
.toURI
|
||||
)
|
||||
val engineProvider = new EngineReleaseProvider(
|
||||
FakeReleaseProvider(fakeReleasesRoot.resolve("enso"))
|
||||
)
|
||||
val runtimeProvider = new GraalCEReleaseProvider(
|
||||
FakeReleaseProvider(fakeReleasesRoot.resolve("graalvm"))
|
||||
)
|
||||
val componentsManager = new ComponentsManager(
|
||||
GlobalCLIOptions(autoConfirm = true, hideProgress = true),
|
||||
distributionManager,
|
||||
engineProvider,
|
||||
runtimeProvider
|
||||
)
|
||||
|
||||
(distributionManager, componentsManager)
|
||||
}
|
||||
|
||||
def makeComponentsManager(): ComponentsManager = makeManagers()._2
|
||||
class ComponentsManagerSpec extends ComponentsManagerTest {
|
||||
|
||||
"ComponentsManager" should {
|
||||
"find the latest engine version in semver ordering" in {
|
||||
@ -59,7 +15,7 @@ class ComponentsManagerSpec
|
||||
|
||||
"install the engine and a matching runtime for it" in {
|
||||
Logger.suppressWarnings {
|
||||
val (distributionManager, componentsManager) = makeManagers()
|
||||
val (distributionManager, componentsManager, _) = makeManagers()
|
||||
|
||||
val version = SemVer(0, 0, 1)
|
||||
val engine = componentsManager.findOrInstallEngine(SemVer(0, 0, 1))
|
||||
|
@ -0,0 +1,69 @@
|
||||
package org.enso.launcher.components
|
||||
|
||||
import java.nio.file.Path
|
||||
|
||||
import org.enso.launcher.cli.GlobalCLIOptions
|
||||
import org.enso.launcher.installation.DistributionManager
|
||||
import org.enso.launcher.releases.{
|
||||
EngineReleaseProvider,
|
||||
GraalCEReleaseProvider
|
||||
}
|
||||
import org.enso.launcher.{Environment, FakeEnvironment, WithTemporaryDirectory}
|
||||
import org.scalatest.OptionValues
|
||||
import org.scalatest.matchers.should.Matchers
|
||||
import org.scalatest.wordspec.AnyWordSpec
|
||||
|
||||
class ComponentsManagerTest
|
||||
extends AnyWordSpec
|
||||
with Matchers
|
||||
with OptionValues
|
||||
with WithTemporaryDirectory
|
||||
with FakeEnvironment {
|
||||
|
||||
/**
|
||||
* Creates the [[DistributionManager]], [[ComponentsManager]] and an
|
||||
* [[Environment]] for use in the tests.
|
||||
*
|
||||
* Should be called separately for each test case, as the components use
|
||||
* temporary directories which are separate for each test case.
|
||||
*
|
||||
* Additional environment variables may be provided that are added to the
|
||||
* [[Environment]] for the created managers.
|
||||
*/
|
||||
def makeManagers(
|
||||
environmentOverrides: Map[String, String] = Map.empty
|
||||
): (DistributionManager, ComponentsManager, Environment) = {
|
||||
val env = fakeInstalledEnvironment(environmentOverrides)
|
||||
val distributionManager = new DistributionManager(env)
|
||||
val fakeReleasesRoot =
|
||||
Path.of(
|
||||
getClass
|
||||
.getResource("/org/enso/launcher/components/fake-releases")
|
||||
.toURI
|
||||
)
|
||||
val engineProvider = new EngineReleaseProvider(
|
||||
FakeReleaseProvider(
|
||||
fakeReleasesRoot.resolve("enso"),
|
||||
copyIntoArchiveRoot = Seq("manifest.yaml")
|
||||
)
|
||||
)
|
||||
val runtimeProvider = new GraalCEReleaseProvider(
|
||||
FakeReleaseProvider(fakeReleasesRoot.resolve("graalvm"))
|
||||
)
|
||||
val componentsManager = new ComponentsManager(
|
||||
GlobalCLIOptions(autoConfirm = true, hideProgress = true),
|
||||
distributionManager,
|
||||
engineProvider,
|
||||
runtimeProvider
|
||||
)
|
||||
|
||||
(distributionManager, componentsManager, env)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns just the [[ComponentsManager]].
|
||||
*
|
||||
* See [[makeManagers]] for details.
|
||||
*/
|
||||
def makeComponentsManager(): ComponentsManager = makeManagers()._2
|
||||
}
|
@ -1,22 +1,36 @@
|
||||
package org.enso.launcher.components
|
||||
|
||||
import java.nio.file.{Files, Path}
|
||||
import java.nio.file.{Files, Path, StandardCopyOption}
|
||||
|
||||
import org.enso.cli.{ProgressListener, TaskProgress}
|
||||
import org.enso.launcher.{FileSystem, OS}
|
||||
import org.enso.launcher.releases.{
|
||||
Asset,
|
||||
Release,
|
||||
ReleaseProvider,
|
||||
ReleaseProviderException
|
||||
}
|
||||
import org.enso.launcher.{FileSystem, OS}
|
||||
|
||||
import scala.io.Source
|
||||
import scala.sys.process._
|
||||
import scala.util.{Success, Try, Using}
|
||||
import sys.process._
|
||||
|
||||
case class FakeReleaseProvider(releasesRoot: Path) extends ReleaseProvider {
|
||||
private val releases = FileSystem.listDirectory(releasesRoot).map(FakeRelease)
|
||||
/**
|
||||
* A release provider that creates fake releases from the defined resources.
|
||||
*
|
||||
* @param releasesRoot path to the directory containing subdirectories for each
|
||||
* release
|
||||
* @param copyIntoArchiveRoot list of filenames that will be copied to the root
|
||||
* of each created archive
|
||||
*/
|
||||
case class FakeReleaseProvider(
|
||||
releasesRoot: Path,
|
||||
copyIntoArchiveRoot: Seq[String] = Seq.empty
|
||||
) extends ReleaseProvider {
|
||||
private val releases =
|
||||
FileSystem
|
||||
.listDirectory(releasesRoot)
|
||||
.map(FakeRelease(_, copyIntoArchiveRoot))
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
@ -33,7 +47,15 @@ case class FakeReleaseProvider(releasesRoot: Path) extends ReleaseProvider {
|
||||
override def listReleases(): Try[Seq[Release]] = Success(releases)
|
||||
}
|
||||
|
||||
case class FakeRelease(path: Path) extends Release {
|
||||
/**
|
||||
* The release created by [[FakeReleaseProvider]].
|
||||
*
|
||||
* @param path path to the release root, each file or directory inside of it
|
||||
* represents a [[FakeAsset]]
|
||||
* @param copyIntoArchiveRoot list of
|
||||
*/
|
||||
case class FakeRelease(path: Path, copyIntoArchiveRoot: Seq[String] = Seq.empty)
|
||||
extends Release {
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
@ -43,11 +65,26 @@ case class FakeRelease(path: Path) extends Release {
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
override def assets: Seq[Asset] =
|
||||
FileSystem.listDirectory(path).map(FakeAsset)
|
||||
override def assets: Seq[Asset] = {
|
||||
val pathsToCopy = copyIntoArchiveRoot.map(path.resolve)
|
||||
FileSystem.listDirectory(path).map(FakeAsset(_, pathsToCopy))
|
||||
}
|
||||
}
|
||||
|
||||
case class FakeAsset(source: Path) extends Asset {
|
||||
/**
|
||||
* Represents an asset of the [[FakeRelease]].
|
||||
*
|
||||
* If it is a file, 'downloading' it just copies it to the destination.
|
||||
* Fetching it reads it as text.
|
||||
*
|
||||
* If it is a directory, it is treated as a fake archive - an archive with all
|
||||
* files contained within that directory is created at the specified
|
||||
* destination. Any paths from `copyIntoArchiveRoot` are also added into the
|
||||
* root of that created archive. This allows to avoid maintaining additional
|
||||
* copies of shared files like the manifest.
|
||||
*/
|
||||
case class FakeAsset(source: Path, copyIntoArchiveRoot: Seq[Path] = Seq.empty)
|
||||
extends Asset {
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
@ -69,33 +106,62 @@ case class FakeAsset(source: Path) extends Asset {
|
||||
}
|
||||
|
||||
private def copyFakeAsset(destination: Path): Unit =
|
||||
if (Files.isDirectory(source)) {
|
||||
val directoryName = source.getFileName.toString
|
||||
if (directoryName.endsWith(".tar.gz") && OS.isUNIX)
|
||||
packTarGz(source, destination)
|
||||
else if (directoryName.endsWith(".zip") && OS.isWindows)
|
||||
packZip(source, destination)
|
||||
else {
|
||||
throw new IllegalArgumentException(
|
||||
s"Fake-archive format $directoryName is not supported on " +
|
||||
s"${OS.operatingSystem}."
|
||||
if (Files.isDirectory(source))
|
||||
copyArchive(destination)
|
||||
else
|
||||
copyNormalFile(destination)
|
||||
|
||||
private def copyArchive(destination: Path): Unit = {
|
||||
val directoryName = source.getFileName.toString
|
||||
lazy val innerRoot = {
|
||||
val roots = FileSystem.listDirectory(source).filter(Files.isDirectory(_))
|
||||
if (roots.length > 1) {
|
||||
throw new IllegalStateException(
|
||||
"Cannot copy files into the root if there are more than one root."
|
||||
)
|
||||
}
|
||||
} else {
|
||||
FileSystem.copyFile(source, destination)
|
||||
|
||||
roots.headOption.getOrElse(source)
|
||||
}
|
||||
|
||||
for (sourceToCopy <- copyIntoArchiveRoot) {
|
||||
Files.copy(
|
||||
sourceToCopy,
|
||||
innerRoot.resolve(sourceToCopy.getFileName),
|
||||
StandardCopyOption.REPLACE_EXISTING
|
||||
)
|
||||
}
|
||||
if (directoryName.endsWith(".tar.gz") && OS.isUNIX)
|
||||
packTarGz(source, destination)
|
||||
else if (directoryName.endsWith(".zip") && OS.isWindows)
|
||||
packZip(source, destination)
|
||||
else {
|
||||
throw new IllegalArgumentException(
|
||||
s"Fake-archive format $directoryName is not supported on " +
|
||||
s"${OS.operatingSystem}."
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private def copyNormalFile(destination: Path): Unit =
|
||||
FileSystem.copyFile(source, destination)
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
override def fetchAsText(): TaskProgress[String] = {
|
||||
val txt = Using(Source.fromFile(source.toFile)) { src =>
|
||||
src.getLines().mkString("\n")
|
||||
override def fetchAsText(): TaskProgress[String] =
|
||||
if (Files.isDirectory(source))
|
||||
throw new IllegalStateException(
|
||||
"Cannot fetch a fake archive (a directory) as text."
|
||||
)
|
||||
else {
|
||||
val txt = Using(Source.fromFile(source.toFile)) { src =>
|
||||
src.getLines().mkString("\n")
|
||||
}
|
||||
(listener: ProgressListener[String]) => {
|
||||
listener.done(txt)
|
||||
}
|
||||
}
|
||||
(listener: ProgressListener[String]) => {
|
||||
listener.done(txt)
|
||||
}
|
||||
}
|
||||
|
||||
private def packTarGz(source: Path, destination: Path): Unit = {
|
||||
val files = FileSystem.listDirectory(source)
|
||||
|
@ -0,0 +1,358 @@
|
||||
package org.enso.launcher.components.runner
|
||||
|
||||
import java.nio.file.{Files, Path}
|
||||
import java.util.UUID
|
||||
|
||||
import nl.gn0s1s.bump.SemVer
|
||||
import org.enso.launcher.FileSystem.PathSyntax
|
||||
import org.enso.launcher.{GlobalConfigurationManager, Logger}
|
||||
import org.enso.launcher.components.ComponentsManagerTest
|
||||
import org.enso.launcher.project.ProjectManager
|
||||
|
||||
class RunnerSpec extends ComponentsManagerTest {
|
||||
private val defaultEngineVersion = SemVer(0, 0, 0, Some("default"))
|
||||
case class TestSetup(runner: Runner, projectManager: ProjectManager)
|
||||
def makeFakeRunner(
|
||||
cwdOverride: Option[Path] = None,
|
||||
extraEnv: Map[String, String] = Map.empty
|
||||
): TestSetup = {
|
||||
val (_, componentsManager, env) = makeManagers(extraEnv)
|
||||
val configurationManager =
|
||||
new GlobalConfigurationManager(componentsManager) {
|
||||
override def defaultVersion: SemVer = defaultEngineVersion
|
||||
}
|
||||
val projectManager = new ProjectManager(configurationManager)
|
||||
val cwd = cwdOverride.getOrElse(getTestDirectory)
|
||||
val runner =
|
||||
new Runner(projectManager, configurationManager, componentsManager, env) {
|
||||
override protected val currentWorkingDirectory: Path = cwd
|
||||
}
|
||||
TestSetup(runner, projectManager)
|
||||
}
|
||||
|
||||
"Runner" should {
|
||||
"create a command from settings" in {
|
||||
Logger.suppressWarnings {
|
||||
val envOptions = "-Xfrom-env -Denv=env"
|
||||
val TestSetup(runner, _) =
|
||||
makeFakeRunner(extraEnv = Map("ENSO_JVM_OPTS" -> envOptions))
|
||||
|
||||
val runSettings = RunSettings(SemVer(0, 0, 0), Seq("arg1", "--flag2"))
|
||||
val jvmOptions = Seq(("locally-added-options", "value1"))
|
||||
val systemCommand = runner.createCommand(
|
||||
runSettings,
|
||||
JVMSettings(useSystemJVM = true, jvmOptions = jvmOptions)
|
||||
)
|
||||
|
||||
systemCommand.command.head shouldEqual "java"
|
||||
|
||||
val managedCommand = runner.createCommand(
|
||||
runSettings,
|
||||
JVMSettings(useSystemJVM = false, jvmOptions = jvmOptions)
|
||||
)
|
||||
|
||||
managedCommand.command.head should include("java")
|
||||
managedCommand.extraEnv.find(_._1 == "JAVA_HOME").value._2 should
|
||||
include("graalvm-ce")
|
||||
|
||||
val enginePath =
|
||||
getTestDirectory / "test_data" / "dist" / "0.0.0"
|
||||
val runtimePath =
|
||||
(enginePath / "component" / "runtime.jar").toAbsolutePath.normalize
|
||||
val runnerPath =
|
||||
(enginePath / "component" / "runner.jar").toAbsolutePath.normalize
|
||||
|
||||
for (command <- Seq(systemCommand, managedCommand)) {
|
||||
val commandLine = command.command.mkString(" ")
|
||||
val arguments = command.command.tail
|
||||
arguments should contain("-Xfrom-env")
|
||||
arguments should contain("-Denv=env")
|
||||
arguments should contain("-Dlocally-added-options=value1")
|
||||
arguments should contain("-Dlocally-added-options=value1")
|
||||
arguments should contain("-Doptions-added-from-manifest=42")
|
||||
arguments should contain("-Xanother-one")
|
||||
commandLine should endWith("arg1 --flag2")
|
||||
|
||||
arguments should contain(s"-Dtruffle.class.path.append=$runtimePath")
|
||||
arguments.filter(
|
||||
_.contains("truffle.class.path.append")
|
||||
) should have length 1
|
||||
|
||||
commandLine should include(s"-jar $runnerPath")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
"run repl with default version and additional arguments" in {
|
||||
val TestSetup(runner, _) = makeFakeRunner()
|
||||
val runSettings = runner
|
||||
.repl(
|
||||
projectPath = None,
|
||||
versionOverride = None,
|
||||
additionalArguments = Seq("arg", "--flag")
|
||||
)
|
||||
.get
|
||||
|
||||
runSettings.version shouldEqual defaultEngineVersion
|
||||
runSettings.runnerArguments should (contain("arg") and contain("--flag"))
|
||||
runSettings.runnerArguments.mkString(" ") should
|
||||
(include("--repl") and not include (s"--in-project"))
|
||||
}
|
||||
|
||||
"run repl in project context" in {
|
||||
val TestSetup(runnerOutside, projectManager) = makeFakeRunner()
|
||||
|
||||
val version = SemVer(0, 0, 0, Some("repl-test"))
|
||||
version should not equal defaultEngineVersion // sanity check
|
||||
|
||||
val projectPath = getTestDirectory / "project"
|
||||
val normalizedPath = projectPath.toAbsolutePath.normalize.toString
|
||||
projectManager.newProject(
|
||||
"test",
|
||||
projectPath,
|
||||
Some(version)
|
||||
)
|
||||
|
||||
val outsideProject = runnerOutside
|
||||
.repl(
|
||||
projectPath = Some(projectPath),
|
||||
versionOverride = None,
|
||||
additionalArguments = Seq()
|
||||
)
|
||||
.get
|
||||
|
||||
outsideProject.version shouldEqual version
|
||||
outsideProject.runnerArguments.mkString(" ") should
|
||||
(include(s"--in-project $normalizedPath") and include("--repl"))
|
||||
|
||||
val TestSetup(runnerInside, _) = makeFakeRunner(Some(projectPath))
|
||||
val insideProject = runnerInside
|
||||
.repl(
|
||||
projectPath = None,
|
||||
versionOverride = None,
|
||||
additionalArguments = Seq()
|
||||
)
|
||||
.get
|
||||
|
||||
insideProject.version shouldEqual version
|
||||
insideProject.runnerArguments.mkString(" ") should
|
||||
(include(s"--in-project $normalizedPath") and include("--repl"))
|
||||
|
||||
val overridden = SemVer(0, 0, 0, Some("overridden"))
|
||||
val overriddenRun = runnerInside
|
||||
.repl(
|
||||
projectPath = Some(projectPath),
|
||||
versionOverride = Some(overridden),
|
||||
additionalArguments = Seq()
|
||||
)
|
||||
.get
|
||||
|
||||
overriddenRun.version shouldEqual overridden
|
||||
overriddenRun.runnerArguments.mkString(" ") should
|
||||
(include(s"--in-project $normalizedPath") and include("--repl"))
|
||||
}
|
||||
|
||||
"run language server" in {
|
||||
val TestSetup(runner, projectManager) = makeFakeRunner()
|
||||
|
||||
val version = SemVer(0, 0, 0, Some("language-server-test"))
|
||||
val projectPath = getTestDirectory / "project"
|
||||
projectManager.newProject(
|
||||
"test",
|
||||
projectPath,
|
||||
Some(version)
|
||||
)
|
||||
|
||||
val options = LanguageServerOptions(
|
||||
rootId = UUID.randomUUID(),
|
||||
path = projectPath,
|
||||
interface = "127.0.0.2",
|
||||
rpcPort = 1234,
|
||||
dataPort = 4321
|
||||
)
|
||||
val runSettings = runner
|
||||
.languageServer(
|
||||
options,
|
||||
versionOverride = None,
|
||||
additionalArguments = Seq("additional")
|
||||
)
|
||||
.get
|
||||
|
||||
runSettings.version shouldEqual version
|
||||
val commandLine = runSettings.runnerArguments.mkString(" ")
|
||||
commandLine should include(s"--interface ${options.interface}")
|
||||
commandLine should include(s"--rpc-port ${options.rpcPort}")
|
||||
commandLine should include(s"--data-port ${options.dataPort}")
|
||||
commandLine should include(s"--root-id ${options.rootId}")
|
||||
val normalizedPath = options.path.toAbsolutePath.normalize.toString
|
||||
commandLine should include(s"--path $normalizedPath")
|
||||
runSettings.runnerArguments.lastOption.value shouldEqual "additional"
|
||||
|
||||
val overridden = SemVer(0, 0, 0, Some("overridden"))
|
||||
runner
|
||||
.languageServer(
|
||||
options,
|
||||
versionOverride = Some(overridden),
|
||||
additionalArguments = Seq()
|
||||
)
|
||||
.get
|
||||
.version shouldEqual overridden
|
||||
}
|
||||
|
||||
"run a project" in {
|
||||
val TestSetup(runnerOutside, projectManager) = makeFakeRunner()
|
||||
|
||||
val version = SemVer(0, 0, 0, Some("run-test"))
|
||||
val projectPath = getTestDirectory / "project"
|
||||
val normalizedPath = projectPath.toAbsolutePath.normalize.toString
|
||||
projectManager.newProject(
|
||||
"test",
|
||||
projectPath,
|
||||
Some(version)
|
||||
)
|
||||
|
||||
val outsideProject = runnerOutside
|
||||
.run(
|
||||
path = Some(projectPath),
|
||||
versionOverride = None,
|
||||
additionalArguments = Seq()
|
||||
)
|
||||
.get
|
||||
|
||||
outsideProject.version shouldEqual version
|
||||
outsideProject.runnerArguments.mkString(" ") should
|
||||
include(s"--run $normalizedPath")
|
||||
|
||||
val TestSetup(runnerInside, _) = makeFakeRunner(Some(projectPath))
|
||||
val insideProject = runnerInside
|
||||
.run(
|
||||
path = None,
|
||||
versionOverride = None,
|
||||
additionalArguments = Seq()
|
||||
)
|
||||
.get
|
||||
|
||||
insideProject.version shouldEqual version
|
||||
insideProject.runnerArguments.mkString(" ") should
|
||||
include(s"--run $normalizedPath")
|
||||
|
||||
val overridden = SemVer(0, 0, 0, Some("overridden"))
|
||||
val overriddenRun = runnerInside
|
||||
.run(
|
||||
path = Some(projectPath),
|
||||
versionOverride = Some(overridden),
|
||||
additionalArguments = Seq()
|
||||
)
|
||||
.get
|
||||
|
||||
overriddenRun.version shouldEqual overridden
|
||||
overriddenRun.runnerArguments.mkString(" ") should
|
||||
include(s"--run $normalizedPath")
|
||||
|
||||
assert(
|
||||
runnerOutside
|
||||
.run(
|
||||
path = None,
|
||||
versionOverride = None,
|
||||
additionalArguments = Seq()
|
||||
)
|
||||
.isFailure,
|
||||
"Running outside project without providing any paths should be an error"
|
||||
)
|
||||
}
|
||||
|
||||
"run a script outside of a project even if cwd is inside project" in {
|
||||
val version = SemVer(0, 0, 0, Some("run-test"))
|
||||
val projectPath = getTestDirectory / "project"
|
||||
val TestSetup(runnerInside, projectManager) =
|
||||
makeFakeRunner(cwdOverride = Some(projectPath))
|
||||
projectManager.newProject(
|
||||
"test",
|
||||
projectPath,
|
||||
Some(version)
|
||||
)
|
||||
|
||||
val outsideFile = getTestDirectory / "Main.enso"
|
||||
val normalizedPath = outsideFile.toAbsolutePath.normalize.toString
|
||||
Files.copy(
|
||||
projectPath / "src" / "Main.enso",
|
||||
outsideFile
|
||||
)
|
||||
|
||||
val runSettings = runnerInside
|
||||
.run(
|
||||
path = Some(outsideFile),
|
||||
versionOverride = None,
|
||||
additionalArguments = Seq()
|
||||
)
|
||||
.get
|
||||
|
||||
runSettings.version shouldEqual defaultEngineVersion
|
||||
runSettings.runnerArguments.mkString(" ") should
|
||||
(include(s"--run $normalizedPath") and (not(include("--in-project"))))
|
||||
}
|
||||
|
||||
"run a script inside of a project" in {
|
||||
val version = SemVer(0, 0, 0, Some("run-test"))
|
||||
val projectPath = getTestDirectory / "project"
|
||||
val normalizedProjectPath = projectPath.toAbsolutePath.normalize.toString
|
||||
val TestSetup(runnerOutside, projectManager) = makeFakeRunner()
|
||||
projectManager.newProject(
|
||||
"test",
|
||||
projectPath,
|
||||
Some(version)
|
||||
)
|
||||
|
||||
val insideFile = projectPath / "src" / "Main.enso"
|
||||
val normalizedFilePath = insideFile.toAbsolutePath.normalize.toString
|
||||
|
||||
val runSettings = runnerOutside
|
||||
.run(
|
||||
path = Some(insideFile),
|
||||
versionOverride = None,
|
||||
additionalArguments = Seq()
|
||||
)
|
||||
.get
|
||||
|
||||
runSettings.version shouldEqual version
|
||||
runSettings.runnerArguments.mkString(" ") should
|
||||
(include(s"--run $normalizedFilePath") and
|
||||
include(s"--in-project $normalizedProjectPath"))
|
||||
}
|
||||
|
||||
"get default version outside of project" in {
|
||||
val TestSetup(runner, _) = makeFakeRunner()
|
||||
val (runSettings, whichEngine) = runner
|
||||
.version(useJSON = true)
|
||||
.get
|
||||
|
||||
runSettings.version shouldEqual defaultEngineVersion
|
||||
runSettings.runnerArguments should
|
||||
(contain("--version") and contain("--json"))
|
||||
|
||||
whichEngine shouldEqual WhichEngine.Default
|
||||
}
|
||||
|
||||
"get project version inside of project" in {
|
||||
val version = SemVer(0, 0, 0, Some("version-test"))
|
||||
val projectPath = getTestDirectory / "project"
|
||||
val name = "Testname"
|
||||
val TestSetup(runnerInside, projectManager) =
|
||||
makeFakeRunner(cwdOverride = Some(projectPath))
|
||||
projectManager.newProject(
|
||||
name,
|
||||
projectPath,
|
||||
Some(version)
|
||||
)
|
||||
val (runSettings, whichEngine) = runnerInside
|
||||
.version(useJSON = false)
|
||||
.get
|
||||
|
||||
runSettings.version shouldEqual version
|
||||
runSettings.runnerArguments should
|
||||
(contain("--version") and not(contain("--json")))
|
||||
|
||||
whichEngine shouldEqual WhichEngine.FromProject(name)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,47 @@
|
||||
package org.enso.launcher.project
|
||||
|
||||
import nl.gn0s1s.bump.SemVer
|
||||
import org.enso.launcher.{GlobalConfigurationManager, WithTemporaryDirectory}
|
||||
import org.scalatest.Inside
|
||||
import org.scalatest.matchers.should.Matchers
|
||||
import org.scalatest.wordspec.AnyWordSpec
|
||||
|
||||
class ProjectManagerSpec
|
||||
extends AnyWordSpec
|
||||
with Matchers
|
||||
with WithTemporaryDirectory
|
||||
with Inside {
|
||||
private val defaultEnsoVersion = SemVer(0, 0, 0, Some("default"))
|
||||
def makeProjectManager(): ProjectManager = {
|
||||
val fakeConfigurationManager = new GlobalConfigurationManager(null) {
|
||||
override def defaultVersion: SemVer = defaultEnsoVersion
|
||||
}
|
||||
new ProjectManager(fakeConfigurationManager)
|
||||
}
|
||||
|
||||
"ProjectManager" should {
|
||||
"create a new project with correct structure" in {
|
||||
val projectManager = makeProjectManager()
|
||||
val projectDir = getTestDirectory.resolve("proj1")
|
||||
projectManager.newProject("Test Project", projectDir)
|
||||
|
||||
projectDir.toFile should exist
|
||||
projectDir.resolve("src").resolve("Main.enso").toFile should exist
|
||||
|
||||
val project = projectManager.loadProject(projectDir).get
|
||||
project.version shouldEqual defaultEnsoVersion
|
||||
}
|
||||
|
||||
"find projects in parent directories" in {
|
||||
val projectManager = makeProjectManager()
|
||||
val projectDir = getTestDirectory.resolve("proj1")
|
||||
projectManager.newProject("Test Project", projectDir)
|
||||
|
||||
projectManager.findProject(projectDir).get should be(defined)
|
||||
projectManager.findProject(projectDir.resolve("src")).get should
|
||||
be(defined)
|
||||
|
||||
projectManager.findProject(getTestDirectory).get should be(empty)
|
||||
}
|
||||
}
|
||||
}
|
@ -28,6 +28,7 @@ object Main {
|
||||
private val DATA_PORT_OPTION = "data-port"
|
||||
private val ROOT_ID_OPTION = "root-id"
|
||||
private val ROOT_PATH_OPTION = "path"
|
||||
private val IN_PROJECT_OPTION = "in-project"
|
||||
private val VERSION_OPTION = "version"
|
||||
private val JSON_OPTION = "json"
|
||||
|
||||
@ -99,6 +100,16 @@ object Main {
|
||||
.longOpt(ROOT_PATH_OPTION)
|
||||
.desc("Path to the content root.")
|
||||
.build()
|
||||
val inProjectOption = CliOption.builder
|
||||
.hasArg(true)
|
||||
.numberOfArgs(1)
|
||||
.argName("project-path")
|
||||
.longOpt(IN_PROJECT_OPTION)
|
||||
.desc(
|
||||
"Setting this option when running the REPL or an Enso script, runs it" +
|
||||
"in context of the specified project."
|
||||
)
|
||||
.build()
|
||||
val version = CliOption.builder
|
||||
.longOpt(VERSION_OPTION)
|
||||
.desc("Checks the version of the Enso executable.")
|
||||
@ -120,6 +131,7 @@ object Main {
|
||||
.addOption(dataPortOption)
|
||||
.addOption(uuidOption)
|
||||
.addOption(pathOption)
|
||||
.addOption(inProjectOption)
|
||||
.addOption(version)
|
||||
.addOption(json)
|
||||
|
||||
@ -135,13 +147,10 @@ object Main {
|
||||
new HelpFormatter().printHelp(LanguageInfo.ID, options)
|
||||
|
||||
/** Terminates the process with a failure exit code. */
|
||||
private def exitFail(): Nothing = {
|
||||
System.exit(1)
|
||||
throw new IllegalStateException("impossible to reach here")
|
||||
}
|
||||
private def exitFail(): Nothing = sys.exit(1)
|
||||
|
||||
/** Terminates the process with a success exit code. */
|
||||
private def exitSuccess(): Unit = System.exit(0)
|
||||
private def exitSuccess(): Unit = sys.exit(0)
|
||||
|
||||
/**
|
||||
* Handles the `--new` CLI option.
|
||||
@ -156,9 +165,14 @@ object Main {
|
||||
/**
|
||||
* Handles the `--run` CLI option.
|
||||
*
|
||||
* If `path` is a directory, so a project is run, a conflicting (pointing to
|
||||
* another project) `projectPath` should not be provided.
|
||||
*
|
||||
* @param path path of the project or file to execute
|
||||
* @param projectPath if specified, the script is run in context of a
|
||||
* project located at that path
|
||||
*/
|
||||
private def run(path: String): Unit = {
|
||||
private def run(path: String, projectPath: Option[String]): Unit = {
|
||||
val file = new File(path)
|
||||
if (!file.exists) {
|
||||
println(s"File $file does not exist.")
|
||||
@ -166,8 +180,19 @@ object Main {
|
||||
}
|
||||
val projectMode = file.isDirectory
|
||||
val packagePath =
|
||||
if (projectMode) file.getAbsolutePath
|
||||
else ""
|
||||
if (projectMode) {
|
||||
projectPath match {
|
||||
case Some(inProject) if inProject != path =>
|
||||
println(
|
||||
"It is not possible to run a project in context of another " +
|
||||
"project, please do not use the `--in-project` option for " +
|
||||
"running projects."
|
||||
)
|
||||
exitFail()
|
||||
case _ =>
|
||||
}
|
||||
file.getAbsolutePath
|
||||
} else projectPath.getOrElse("")
|
||||
val context = new ContextFactory().create(
|
||||
packagePath,
|
||||
System.in,
|
||||
@ -258,9 +283,13 @@ object Main {
|
||||
}
|
||||
}
|
||||
|
||||
private def runMain(mainModule: Module, rootPkgPath: Option[File]): Value = {
|
||||
private def runMain(
|
||||
mainModule: Module,
|
||||
rootPkgPath: Option[File],
|
||||
mainMethodName: String = "main"
|
||||
): Value = {
|
||||
val mainCons = mainModule.getAssociatedConstructor
|
||||
val mainFun = mainModule.getMethod(mainCons, "main")
|
||||
val mainFun = mainModule.getMethod(mainCons, mainMethodName)
|
||||
try {
|
||||
mainFun.execute(mainCons.newInstance())
|
||||
} catch {
|
||||
@ -272,15 +301,25 @@ object Main {
|
||||
|
||||
/**
|
||||
* Handles the `--repl` CLI option
|
||||
*
|
||||
* @param projectPath if specified, the REPL is run in context of a project
|
||||
* at the given path
|
||||
*/
|
||||
private def runRepl(): Unit = {
|
||||
val dummySourceToTriggerRepl = "main = Debug.breakpoint"
|
||||
val replModuleName = "Repl"
|
||||
private def runRepl(projectPath: Option[String]): Unit = {
|
||||
val mainMethodName = "internal_repl_entry_point___"
|
||||
val dummySourceToTriggerRepl = s"$mainMethodName = Debug.breakpoint"
|
||||
val replModuleName = "Internal_Repl_Module___"
|
||||
val packagePath = projectPath.getOrElse("")
|
||||
val context =
|
||||
new ContextFactory().create("", System.in, System.out, Repl(TerminalIO()))
|
||||
new ContextFactory().create(
|
||||
packagePath,
|
||||
System.in,
|
||||
System.out,
|
||||
Repl(TerminalIO())
|
||||
)
|
||||
val mainModule =
|
||||
context.evalModule(dummySourceToTriggerRepl, replModuleName)
|
||||
runMain(mainModule, None)
|
||||
runMain(mainModule, None, mainMethodName = mainMethodName)
|
||||
exitSuccess()
|
||||
}
|
||||
|
||||
@ -366,10 +405,13 @@ object Main {
|
||||
createNew(line.getOptionValue(NEW_OPTION))
|
||||
}
|
||||
if (line.hasOption(RUN_OPTION)) {
|
||||
run(line.getOptionValue(RUN_OPTION))
|
||||
run(
|
||||
line.getOptionValue(RUN_OPTION),
|
||||
Option(line.getOptionValue(IN_PROJECT_OPTION))
|
||||
)
|
||||
}
|
||||
if (line.hasOption(REPL_OPTION)) {
|
||||
runRepl()
|
||||
runRepl(Option(line.getOptionValue(IN_PROJECT_OPTION)))
|
||||
}
|
||||
if (line.hasOption(LANGUAGE_SERVER_OPTION)) {
|
||||
runLanguageServer(line)
|
||||
|
@ -1,5 +1,6 @@
|
||||
name: TestNonImportedOverloads
|
||||
license: APLv2
|
||||
enso-version: default
|
||||
version: "0.0.1"
|
||||
author: "Enso Team <contact@enso.org>"
|
||||
maintainer: "Enso Team <contact@enso.org>"
|
||||
|
@ -1,5 +1,6 @@
|
||||
name: TestNonImportedOwnMethods
|
||||
license: APLv2
|
||||
enso-version: default
|
||||
version: "0.0.1"
|
||||
author: "Enso Team <contact@enso.org>"
|
||||
maintainer: "Enso Team <contact@enso.org>"
|
||||
|
@ -1,5 +1,6 @@
|
||||
name: TestSimpleImports
|
||||
license: APLv2
|
||||
enso-version: default
|
||||
version: "0.0.1"
|
||||
author: "Enso Team <contact@enso.org>"
|
||||
maintainer: "Enso Team <contact@enso.org>"
|
||||
|
@ -1,5 +1,6 @@
|
||||
name: Test_Hiding_Error
|
||||
license: APLv2
|
||||
enso-version: default
|
||||
version: "0.0.1"
|
||||
author: "Enso Team <contact@enso.org>"
|
||||
maintainer: "Enso Team <contact@enso.org>"
|
||||
|
@ -1,5 +1,6 @@
|
||||
name: Test_Hiding_Success
|
||||
license: APLv2
|
||||
enso-version: default
|
||||
version: "0.0.1"
|
||||
author: "Enso Team <contact@enso.org>"
|
||||
maintainer: "Enso Team <contact@enso.org>"
|
||||
|
@ -1,5 +1,6 @@
|
||||
name: Test_Qualified_Error
|
||||
license: APLv2
|
||||
enso-version: default
|
||||
version: "0.0.1"
|
||||
author: "Enso Team <contact@enso.org>"
|
||||
maintainer: "Enso Team <contact@enso.org>"
|
||||
|
@ -1,5 +1,6 @@
|
||||
name: Test_Rename
|
||||
license: APLv2
|
||||
enso-version: default
|
||||
version: "0.0.1"
|
||||
author: "Enso Team <contact@enso.org>"
|
||||
maintainer: "Enso Team <contact@enso.org>"
|
||||
|
@ -1,5 +1,6 @@
|
||||
name: Test_Rename_Error
|
||||
license: APLv2
|
||||
enso-version: default
|
||||
version: "0.0.1"
|
||||
author: "Enso Team <contact@enso.org>"
|
||||
maintainer: "Enso Team <contact@enso.org>"
|
||||
|
@ -71,8 +71,8 @@ class Application[Config](
|
||||
topLevelOpts,
|
||||
tokens,
|
||||
Seq(),
|
||||
isTopLevel = true,
|
||||
command = Seq(commandName)
|
||||
isTopLevel = true,
|
||||
commandPrefix = Seq(commandName)
|
||||
)
|
||||
topLevelParseResult.flatMap {
|
||||
case (run, restOfTokens) =>
|
||||
|
@ -23,19 +23,8 @@ case class Command[A](
|
||||
*
|
||||
* @param applicationName name of the application for usage
|
||||
*/
|
||||
def help(applicationName: String): String = {
|
||||
val tableDivider = "\t"
|
||||
val usages =
|
||||
opts.commandLines().map(s"$applicationName $name$tableDivider" + _)
|
||||
val firstLine = "Usage: "
|
||||
val padding = " " * firstLine.length
|
||||
val usage =
|
||||
firstLine + usages.head +
|
||||
usages.tail.map("\n" + padding + _).mkString + "\n"
|
||||
|
||||
comment + "\n" + usage +
|
||||
opts.helpExplanations(addHelpOption = true).stripTrailing()
|
||||
}
|
||||
def help(applicationName: String): String =
|
||||
comment + "\n" + opts.help(Seq(applicationName, name))
|
||||
|
||||
/**
|
||||
* Returns a top-level help entry for the application help text. It includes
|
||||
|
@ -67,7 +67,7 @@ trait Opts[A] {
|
||||
* the right moment to do final validation (for example, detecting missing
|
||||
* options).
|
||||
*/
|
||||
private[cli] def result(): Either[List[String], A]
|
||||
private[cli] def result(commandPrefix: Seq[String]): Either[List[String], A]
|
||||
|
||||
/**
|
||||
* Lists options that should be printed in the usage [[commandLines]].
|
||||
@ -191,6 +191,28 @@ trait Opts[A] {
|
||||
val additionalText = if (additional.isEmpty) "" else "\n" + additional
|
||||
optionsHelp + prefixedHelp + additionalText
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a help text for the command, including usage, available options
|
||||
* and any additional help lines.
|
||||
*
|
||||
* @param commandPrefix list of command names that should prefix the
|
||||
* commandline in usage. The first entry in that list
|
||||
* should be the application name and the following
|
||||
* entries are commands/subcommands.
|
||||
*/
|
||||
def help(commandPrefix: Seq[String]): String = {
|
||||
val tableDivider = "\t"
|
||||
val prefix = commandPrefix.mkString(" ")
|
||||
val usages = commandLines().map(s"$prefix$tableDivider" + _)
|
||||
val firstLine = "Usage: "
|
||||
val padding = " " * firstLine.length
|
||||
val usage =
|
||||
firstLine + usages.head +
|
||||
usages.tail.map("\n" + padding + _).mkString + "\n"
|
||||
|
||||
usage + helpExplanations(addHelpOption = true).stripTrailing()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -4,8 +4,9 @@ class AdditionalArguments(helpComment: String) extends BaseOpts[Seq[String]] {
|
||||
var value: Seq[String] = Seq()
|
||||
override private[cli] val additionalArguments = Some(args => value = args)
|
||||
|
||||
override private[cli] def result() = Right(value)
|
||||
override private[cli] def reset(): Unit = value = Seq()
|
||||
override private[cli] def result(commandPrefix: Seq[String]) = Right(value)
|
||||
override private[cli] def reset(): Unit =
|
||||
value = Seq()
|
||||
|
||||
override def availableOptionsHelp(): Seq[String] =
|
||||
if (helpComment.nonEmpty) {
|
||||
|
@ -39,7 +39,7 @@ class Flag(
|
||||
)
|
||||
}
|
||||
|
||||
override private[cli] def result() = value
|
||||
override private[cli] def result(commandPrefix: Seq[String]) = value
|
||||
|
||||
override def availableOptionsHelp(): Seq[String] =
|
||||
short match {
|
||||
|
@ -29,5 +29,6 @@ class HiddenOpts[A](opts: Opts[A]) extends Opts[A] {
|
||||
|
||||
override private[cli] def reset(): Unit = opts.reset()
|
||||
|
||||
override private[cli] def result() = opts.result()
|
||||
override private[cli] def result(commandPrefix: Seq[String]) =
|
||||
opts.result(commandPrefix)
|
||||
}
|
||||
|
@ -37,7 +37,7 @@ class OptionalParameter[A: Argument](
|
||||
)
|
||||
}
|
||||
|
||||
override private[cli] def result() = value
|
||||
override private[cli] def result(commandPrefix: Seq[String]) = value
|
||||
|
||||
override def availableOptionsHelp(): Seq[String] = {
|
||||
Seq(s"[--$name $metavar]\t$helpComment")
|
||||
|
@ -27,7 +27,7 @@ class OptionalPositionalArgument[A: Argument](
|
||||
value = empty
|
||||
}
|
||||
|
||||
override private[cli] def result() =
|
||||
override private[cli] def result(commandPrefix: Seq[String]) =
|
||||
value
|
||||
|
||||
override def additionalHelp(): Seq[String] = helpComment.toSeq
|
||||
|
@ -24,7 +24,9 @@ class OptsMap[A, B](a: Opts[A], f: A => B) extends Opts[B] {
|
||||
|
||||
override private[cli] def reset(): Unit = a.reset()
|
||||
|
||||
override private[cli] def result() = a.result().map(f)
|
||||
override private[cli] def result(
|
||||
commandPrefix: Seq[String]
|
||||
): Either[List[String], B] = a.result(commandPrefix).map(f)
|
||||
|
||||
override def availableOptionsHelp(): Seq[String] = a.availableOptionsHelp()
|
||||
override def availablePrefixedParametersHelp(): Seq[String] =
|
||||
|
@ -42,10 +42,12 @@ class OptsProduct[A, B](lhs: Opts[A], rhs: Opts[B]) extends Opts[(A, B)] {
|
||||
rhs.reset()
|
||||
}
|
||||
|
||||
override private[cli] def result() =
|
||||
override private[cli] def result(
|
||||
commandPrefix: Seq[String]
|
||||
): Either[List[String], (A, B)] =
|
||||
for {
|
||||
l <- lhs.result()
|
||||
r <- rhs.result()
|
||||
l <- lhs.result(commandPrefix)
|
||||
r <- rhs.result(commandPrefix)
|
||||
} yield (l, r)
|
||||
|
||||
override def availableOptionsHelp(): Seq[String] =
|
||||
|
@ -1,7 +1,9 @@
|
||||
package org.enso.cli.internal
|
||||
|
||||
class OptsPure[A](v: A) extends BaseOpts[A] {
|
||||
override private[cli] def result() = Right(v)
|
||||
override private[cli] def result(
|
||||
commandPrefix: Seq[String]
|
||||
): Either[List[String], A] = Right(v)
|
||||
override private[cli] def reset(): Unit = {}
|
||||
|
||||
override def availableOptionsHelp(): Seq[String] = Seq()
|
||||
|
@ -36,7 +36,7 @@ class Parameter[A: Argument](
|
||||
)
|
||||
}
|
||||
|
||||
override private[cli] def result() =
|
||||
override private[cli] def result(commandPrefix: Seq[String]) =
|
||||
value.flatMap {
|
||||
case Some(value) => Right(value)
|
||||
case None => Left(List(s"Missing required parameter $name"))
|
||||
|
@ -96,8 +96,8 @@ object Parser {
|
||||
* parsing top-level options, parsing is stopped to return
|
||||
* the top-level options and possibly continue it with
|
||||
* command options
|
||||
* @param command the sequance of subcommand names, used for displaying help
|
||||
* messages
|
||||
* @param commandPrefix the sequence of subcommand names, used for
|
||||
* displaying help messages
|
||||
* @return returns either the result value of `opts` and remaining tokens or
|
||||
* a list of errors on failure; the remaining tokens are non-empty
|
||||
* only if `isTopLevel` is true
|
||||
@ -107,7 +107,7 @@ object Parser {
|
||||
tokens: Seq[Token],
|
||||
additionalArguments: Seq[String],
|
||||
isTopLevel: Boolean,
|
||||
command: Seq[String]
|
||||
commandPrefix: Seq[String]
|
||||
): Either[List[String], (A, Seq[Token])] = {
|
||||
var parseErrors: List[String] = Nil
|
||||
def addError(error: String): Unit = {
|
||||
@ -243,16 +243,21 @@ object Parser {
|
||||
def appendHelp[T](
|
||||
result: Either[List[String], T]
|
||||
): Either[List[String], T] = {
|
||||
val help = s"See `${command.mkString(" ")} --help` for usage explanation."
|
||||
val help =
|
||||
s"See `${commandPrefix.mkString(" ")} --help` for usage explanation."
|
||||
result match {
|
||||
case Left(errors) => Left(errors ++ Seq(help))
|
||||
case Left(errors) =>
|
||||
val shouldAddHelp = !errors.exists(_.contains("Usage:"))
|
||||
if (shouldAddHelp)
|
||||
Left(errors ++ Seq(help))
|
||||
else Left(errors)
|
||||
case Right(value) => Right(value)
|
||||
}
|
||||
}
|
||||
|
||||
appendHelp(
|
||||
appendErrors(
|
||||
opts.result().map((_, tokenProvider.remaining())),
|
||||
opts.result(commandPrefix).map((_, tokenProvider.remaining())),
|
||||
parseErrors.reverse
|
||||
)
|
||||
)
|
||||
|
@ -27,7 +27,7 @@ class PositionalArgument[A: Argument](
|
||||
value = empty
|
||||
}
|
||||
|
||||
override private[cli] def result() =
|
||||
override private[cli] def result(commandPrefix: Seq[String]) =
|
||||
value.flatMap {
|
||||
case Some(value) => Right(value)
|
||||
case None => Left(List(s"Missing required argument <$metavar>."))
|
||||
|
@ -20,7 +20,8 @@ class PrefixedParameters(
|
||||
currentValue = (name, value) :: currentValue
|
||||
}
|
||||
|
||||
override private[cli] def result() = Right(currentValue.reverse)
|
||||
override private[cli] def result(commandPrefix: Seq[String]) =
|
||||
Right(currentValue.reverse)
|
||||
|
||||
override private[cli] def gatherPrefixedParameters =
|
||||
Seq(prefix -> s"--$prefix.$keyMetavar=$valueMetavar")
|
||||
|
@ -37,7 +37,11 @@ class SubcommandOpt[A](subcommands: NonEmptyList[Subcommand[A]])
|
||||
Spelling
|
||||
.selectClosestMatches(arg, subcommands.toList.map(_.name))
|
||||
val suggestions =
|
||||
if (similar.isEmpty) ""
|
||||
if (similar.isEmpty)
|
||||
"\n\nPossible subcommands are\n" +
|
||||
subcommands.toList
|
||||
.map(CLIOutput.indent + _.name + "\n")
|
||||
.mkString
|
||||
else
|
||||
"\n\nThe most similar subcommands are\n" +
|
||||
similar.map(CLIOutput.indent + _ + "\n").mkString
|
||||
@ -61,14 +65,14 @@ class SubcommandOpt[A](subcommands: NonEmptyList[Subcommand[A]])
|
||||
errors = Nil
|
||||
}
|
||||
|
||||
override private[cli] def result() =
|
||||
override private[cli] def result(commandPrefix: Seq[String]) =
|
||||
if (errors.nonEmpty)
|
||||
Left(errors.reverse)
|
||||
else
|
||||
selectedCommand match {
|
||||
case Some(command) => command.opts.result()
|
||||
case Some(command) => command.opts.result(commandPrefix)
|
||||
case None =>
|
||||
Left(List("Expected a subcommand."))
|
||||
Left(List("Expected a subcommand.", help(commandPrefix)))
|
||||
}
|
||||
|
||||
override def availableOptionsHelp(): Seq[String] =
|
||||
|
@ -24,7 +24,7 @@ class TrailingArguments[A: Argument](
|
||||
value = empty
|
||||
}
|
||||
|
||||
override private[cli] def result() =
|
||||
override private[cli] def result(commandPrefix: Seq[String]) =
|
||||
value.map(_.reverse)
|
||||
|
||||
override def additionalHelp(): Seq[String] = helpComment.toSeq
|
||||
|
@ -23,7 +23,13 @@ class OptsSpec
|
||||
def parse(args: Seq[String]): Either[List[String], A] = {
|
||||
val (tokens, additionalArguments) = Parser.tokenize(args)
|
||||
Parser
|
||||
.parseOpts(opts, tokens, additionalArguments, isTopLevel = false, Seq("???"))
|
||||
.parseOpts(
|
||||
opts,
|
||||
tokens,
|
||||
additionalArguments,
|
||||
isTopLevel = false,
|
||||
Seq("???")
|
||||
)
|
||||
.map(_._1)
|
||||
}
|
||||
|
||||
@ -164,10 +170,10 @@ class OptsSpec
|
||||
}
|
||||
|
||||
"parse when put anywhere between arguments" in {
|
||||
val arg1 = Opts.positionalArgument[String]("arg1")
|
||||
val arg2 = Opts.positionalArgument[String]("arg2")
|
||||
val arg1 = Opts.positionalArgument[String]("arg1")
|
||||
val arg2 = Opts.positionalArgument[String]("arg2")
|
||||
val param = Opts.parameter[String]("p", "x", "help")
|
||||
val opts = (arg1, arg2, param) mapN { (_, _, p) => p }
|
||||
val opts = (arg1, arg2, param) mapN { (_, _, p) => p }
|
||||
opts.parseSuccessfully("arg1 arg2 --p value") shouldEqual "value"
|
||||
opts.parseSuccessfully("arg1 --p value arg2") shouldEqual "value"
|
||||
opts.parseSuccessfully("--p value arg1 arg2") shouldEqual "value"
|
||||
@ -222,13 +228,15 @@ class OptsSpec
|
||||
|
||||
"pure" should {
|
||||
"return its value" in {
|
||||
Opts.pure("A").parseSuccessfully("") shouldEqual("A")
|
||||
Opts.pure("A").parseSuccessfully("") shouldEqual "A"
|
||||
}
|
||||
|
||||
"not consume any options" in {
|
||||
Opts.pure("A").parseFailing("arg").head should include("Unexpected")
|
||||
Opts.pure("A").parseFailing("--flag").head should include("Unknown")
|
||||
Opts.pure("A").parseFailing("--parameter=x").head should include("Unknown")
|
||||
Opts.pure("A").parseFailing("--parameter=x").head should include(
|
||||
"Unknown"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -265,23 +273,31 @@ class OptsSpec
|
||||
opts.parseSuccessfully("cmd1") shouldEqual ((false, (1, false)))
|
||||
|
||||
opts.parseSuccessfully("--flag3 cmd1 --flag1") shouldEqual
|
||||
((true, (1, true)))
|
||||
((true, (1, true)))
|
||||
opts.parseSuccessfully("cmd1 --flag3 --flag1") shouldEqual
|
||||
((true, (1, true)))
|
||||
((true, (1, true)))
|
||||
|
||||
opts.parseFailing("--flag1 cmd1")
|
||||
}
|
||||
|
||||
"handle errors nicely" in {
|
||||
opt.parseFailing("").last should include("Usage:")
|
||||
opt.parseFailing("cmd").head should (include("cmd1") and include("cmd2"))
|
||||
opt.parseFailing("cmd").last should include("--help")
|
||||
}
|
||||
}
|
||||
|
||||
"withDefault" should {
|
||||
"return the default value if the result is missing" in {
|
||||
Opts.optionalArgument[Int]("arg")
|
||||
Opts
|
||||
.optionalArgument[Int]("arg")
|
||||
.withDefault(1)
|
||||
.parseSuccessfully("") shouldEqual 1
|
||||
}
|
||||
|
||||
"return the original value if provided" in {
|
||||
Opts.optionalArgument[Int]("arg")
|
||||
Opts
|
||||
.optionalArgument[Int]("arg")
|
||||
.withDefault(1)
|
||||
.parseSuccessfully("0") shouldEqual 0
|
||||
}
|
||||
|
@ -5,16 +5,37 @@ import io.circe.generic.auto._
|
||||
import io.circe.{yaml, Decoder, Encoder, Json}
|
||||
import io.circe.yaml.Printer
|
||||
|
||||
import scala.util.Try
|
||||
|
||||
case class Dependency(name: String, version: String)
|
||||
|
||||
/**
|
||||
* Represents a package configuration stored in the `package.yaml` file.
|
||||
*
|
||||
* @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 author name and contact information of the package author(s)
|
||||
* @param maintainer name and contact information of current package
|
||||
* maintainer(s)
|
||||
* @param dependencies a list of package dependencies
|
||||
*/
|
||||
case class Config(
|
||||
name: String,
|
||||
version: String,
|
||||
ensoVersion: EnsoVersion,
|
||||
license: String,
|
||||
author: List[String],
|
||||
maintainer: List[String],
|
||||
dependencies: List[Dependency]
|
||||
) {
|
||||
|
||||
/**
|
||||
* Converts the configuration into a YAML representation.
|
||||
*/
|
||||
def toYaml: String =
|
||||
Printer.spaces2.copy(preserveOrder = true).pretty(Config.encoder(this))
|
||||
}
|
||||
@ -23,6 +44,7 @@ object Config {
|
||||
private object JsonFields {
|
||||
val name: String = "name"
|
||||
val version: String = "version"
|
||||
val ensoVersion: String = "enso-version"
|
||||
val license: String = "license"
|
||||
val author: String = "author"
|
||||
val maintainer: String = "maintainer"
|
||||
@ -44,9 +66,11 @@ object Config {
|
||||
|
||||
implicit val decoder: Decoder[Config] = { json =>
|
||||
for {
|
||||
name <- json.get[String](JsonFields.name)
|
||||
version <- json.get[String](JsonFields.version)
|
||||
license <- json.getOrElse(JsonFields.license)("")
|
||||
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[String]](JsonFields.author)(List())(
|
||||
decodeContactsList
|
||||
)
|
||||
@ -59,6 +83,7 @@ object Config {
|
||||
} yield Config(
|
||||
name,
|
||||
version,
|
||||
ensoVersion,
|
||||
license,
|
||||
author,
|
||||
maintainer,
|
||||
@ -68,11 +93,12 @@ object Config {
|
||||
|
||||
implicit val encoder: Encoder[Config] = { config =>
|
||||
val base = Json.obj(
|
||||
JsonFields.name -> config.name.asJson,
|
||||
JsonFields.version -> config.version.asJson,
|
||||
JsonFields.license -> config.license.asJson,
|
||||
JsonFields.author -> encodeContactsList(config.author),
|
||||
JsonFields.maintainer -> encodeContactsList(config.maintainer)
|
||||
JsonFields.name -> config.name.asJson,
|
||||
JsonFields.version -> config.version.asJson,
|
||||
JsonFields.ensoVersion -> config.ensoVersion.asJson,
|
||||
JsonFields.license -> config.license.asJson,
|
||||
JsonFields.author -> encodeContactsList(config.author),
|
||||
JsonFields.maintainer -> encodeContactsList(config.maintainer)
|
||||
)
|
||||
val withDeps =
|
||||
if (config.dependencies.nonEmpty)
|
||||
@ -83,7 +109,7 @@ object Config {
|
||||
withDeps
|
||||
}
|
||||
|
||||
def fromYaml(yamlString: String): Option[Config] = {
|
||||
yaml.parser.parse(yamlString).flatMap(_.as[Config]).toOption
|
||||
def fromYaml(yamlString: String): Try[Config] = {
|
||||
yaml.parser.parse(yamlString).flatMap(_.as[Config]).toTry
|
||||
}
|
||||
}
|
||||
|
66
lib/scala/pkg/src/main/scala/org/enso/pkg/EnsoVersion.scala
Normal file
66
lib/scala/pkg/src/main/scala/org/enso/pkg/EnsoVersion.scala
Normal file
@ -0,0 +1,66 @@
|
||||
package org.enso.pkg
|
||||
|
||||
import io.circe.syntax._
|
||||
import io.circe.{Decoder, DecodingFailure, Encoder}
|
||||
import nl.gn0s1s.bump.SemVer
|
||||
|
||||
/**
|
||||
* Represents the engine version that is associated with a project.
|
||||
*/
|
||||
sealed trait EnsoVersion
|
||||
|
||||
/**
|
||||
* Represents `default` Enso version.
|
||||
*
|
||||
* If `enso-version` is set to `default`, the locally default Enso engine
|
||||
* version is used for the project.
|
||||
*/
|
||||
case object DefaultEnsoVersion extends EnsoVersion {
|
||||
private val defaultEnsoVersion = "default"
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
override def toString: String = defaultEnsoVersion
|
||||
}
|
||||
|
||||
/**
|
||||
* An exact semantic versioning string.
|
||||
*/
|
||||
case class SemVerEnsoVersion(version: SemVer) extends EnsoVersion {
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
override def toString: String = version.toString
|
||||
}
|
||||
|
||||
object EnsoVersion {
|
||||
|
||||
/**
|
||||
* [[Decoder]] instance allowing to parse [[EnsoVersion]].
|
||||
*/
|
||||
implicit val decoder: Decoder[EnsoVersion] = { json =>
|
||||
json.as[String].flatMap { string =>
|
||||
if (string == DefaultEnsoVersion.toString)
|
||||
Right(DefaultEnsoVersion)
|
||||
else
|
||||
SemVer(string)
|
||||
.map(SemVerEnsoVersion)
|
||||
.toRight(
|
||||
DecodingFailure(
|
||||
s"`$string` is not a valid version string. Possible values are " +
|
||||
s"`default` or a semantic versioning string.",
|
||||
json.history
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* [[Encoder]] instance allowing to convert [[EnsoVersion]] to JSON or YAML.
|
||||
*/
|
||||
implicit val encoder: Encoder[EnsoVersion] = { version =>
|
||||
version.toString.asJson
|
||||
}
|
||||
}
|
@ -2,11 +2,13 @@ package org.enso.pkg
|
||||
|
||||
import java.io.File
|
||||
|
||||
import cats.Show
|
||||
|
||||
import scala.jdk.CollectionConverters._
|
||||
import org.enso.filesystem.FileSystem
|
||||
|
||||
import scala.io.Source
|
||||
import scala.util.Try
|
||||
import scala.util.{Failure, Try, Using}
|
||||
|
||||
object CouldNotCreateDirectory extends Exception
|
||||
|
||||
@ -41,7 +43,7 @@ case class Package[F](
|
||||
* Sets the package name.
|
||||
*
|
||||
* @param newName the new package name
|
||||
* @return a packge with the updated name
|
||||
* @return a package with the updated name
|
||||
*/
|
||||
def setPackageName(newName: String): Package[F] =
|
||||
this.copy(config = config.copy(name = newName))
|
||||
@ -200,11 +202,13 @@ class PackageManager[F](implicit val fileSystem: FileSystem[F]) {
|
||||
def create(
|
||||
root: F,
|
||||
name: String,
|
||||
version: String = "0.0.1"
|
||||
version: String = "0.0.1",
|
||||
ensoVersion: EnsoVersion = DefaultEnsoVersion
|
||||
): Package[F] = {
|
||||
val config = Config(
|
||||
name = normalizeName(name),
|
||||
version = version,
|
||||
ensoVersion = ensoVersion,
|
||||
license = "",
|
||||
author = List(),
|
||||
maintainer = List(),
|
||||
@ -217,18 +221,60 @@ class PackageManager[F](implicit val fileSystem: FileSystem[F]) {
|
||||
* Tries to parse package structure from a given root location.
|
||||
*
|
||||
* @param root the root location to get package info from.
|
||||
* @return `Some(pkg)` if the location represents a package, `None` otherwise.
|
||||
* @return `Some(pkg)` if the location represents a package, `None`
|
||||
* otherwise.
|
||||
*/
|
||||
def fromDirectory(root: F): Option[Package[F]] = {
|
||||
if (!root.exists) return None
|
||||
val configFile = root.getChild(Package.configFileName)
|
||||
val reader = Try(configFile.newBufferedReader)
|
||||
val resultStr = reader
|
||||
.flatMap(rd => Try(rd.lines().iterator().asScala.mkString("\n")))
|
||||
.toOption
|
||||
val result = resultStr.flatMap(Config.fromYaml)
|
||||
reader.map(_.close())
|
||||
result.map(Package(root, _, fileSystem))
|
||||
def fromDirectory(root: F): Option[Package[F]] =
|
||||
loadPackage(root).toOption
|
||||
|
||||
/**
|
||||
* Loads the package structure at the given root location and reports any
|
||||
* errors.
|
||||
*
|
||||
* @param root the root location to get package info from.
|
||||
* @return `Success(pkg)` if the location represents a valid package, and
|
||||
* `Failure` otherwise. If the package file or its parent directory do
|
||||
* not exist, a `PackageNotFound` exception is returned. Otherwise,
|
||||
* the exception that made it not possible to load the package is
|
||||
* returned.
|
||||
*/
|
||||
def loadPackage(root: F): Try[Package[F]] = {
|
||||
val result =
|
||||
if (!root.exists) Failure(PackageManager.PackageNotFound())
|
||||
else {
|
||||
def readConfig(file: F): Try[String] =
|
||||
if (file.exists)
|
||||
Using(file.newBufferedReader) { reader =>
|
||||
reader.lines().iterator().asScala.mkString("\n")
|
||||
}
|
||||
else Failure(PackageManager.PackageNotFound())
|
||||
|
||||
val configFile = root.getChild(Package.configFileName)
|
||||
for {
|
||||
resultStr <- readConfig(configFile)
|
||||
result <- Config.fromYaml(resultStr)
|
||||
} yield Package(root, result, fileSystem)
|
||||
}
|
||||
result.recoverWith {
|
||||
case packageLoadingException: PackageManager.PackageLoadingException =>
|
||||
Failure(packageLoadingException)
|
||||
case decodingError: io.circe.Error =>
|
||||
val errorMessage =
|
||||
implicitly[Show[io.circe.Error]].show(decodingError)
|
||||
Failure(
|
||||
PackageManager.PackageLoadingFailure(
|
||||
s"Cannot decode the package config: $errorMessage",
|
||||
decodingError
|
||||
)
|
||||
)
|
||||
case otherError =>
|
||||
Failure(
|
||||
PackageManager.PackageLoadingFailure(
|
||||
s"Cannot load the package: $otherError",
|
||||
otherError
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -270,6 +316,30 @@ class PackageManager[F](implicit val fileSystem: FileSystem[F]) {
|
||||
|
||||
object PackageManager {
|
||||
val Default = new PackageManager[File]()(FileSystem.Default)
|
||||
|
||||
/**
|
||||
* A general exception indicating that a package cannot be loaded.
|
||||
*/
|
||||
class PackageLoadingException(message: String, cause: Throwable)
|
||||
extends RuntimeException(message, cause) {
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
override def toString: String = message
|
||||
}
|
||||
|
||||
/**
|
||||
* The error indicating that the requested package does not exist.
|
||||
*/
|
||||
case class PackageNotFound()
|
||||
extends PackageLoadingException(s"The package file does not exist.", null)
|
||||
|
||||
/**
|
||||
* The error indicating that the package exists, but cannot be loaded.
|
||||
*/
|
||||
case class PackageLoadingFailure(message: String, cause: Throwable)
|
||||
extends PackageLoadingException(message, cause)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -0,0 +1,22 @@
|
||||
package org.enso.pkg
|
||||
|
||||
import io.circe.{Decoder, DecodingFailure}
|
||||
import nl.gn0s1s.bump.SemVer
|
||||
|
||||
object SemVerDecoder {
|
||||
|
||||
/**
|
||||
* [[Decoder]] instance allowing to parse semantic versioning strings.
|
||||
*/
|
||||
implicit val semverDecoder: Decoder[SemVer] = { json =>
|
||||
for {
|
||||
string <- json.as[String]
|
||||
version <- SemVer(string).toRight(
|
||||
DecodingFailure(
|
||||
s"`$string` is not a valid semantic versioning string.",
|
||||
json.history
|
||||
)
|
||||
)
|
||||
} yield version
|
||||
}
|
||||
}
|
@ -10,22 +10,31 @@ trait VersionDescription {
|
||||
if (useJson) asJSONString else asHumanReadableString
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines an additional parameter for the version description.
|
||||
*
|
||||
* @param humanReadableName the human readable prefix added when printing this
|
||||
* parameter in human-readable format
|
||||
* @param jsonName the key when outputting the parameter in JSON format
|
||||
* @param value the value to use for the parameter; depending on if the whole
|
||||
* version description will be queried as a human-readable version
|
||||
* or in JSON, this value should be in the right format
|
||||
*/
|
||||
case class VersionDescriptionParameter(
|
||||
humanReadableName: String,
|
||||
humandReadableValue: String,
|
||||
jsonName: String,
|
||||
jsonValue: String
|
||||
value: String
|
||||
)
|
||||
|
||||
object VersionDescription {
|
||||
def formatParameterAsJSONString(
|
||||
parameter: VersionDescriptionParameter
|
||||
): String =
|
||||
s""""${parameter.jsonName}": ${parameter.jsonValue}"""
|
||||
s""""${parameter.jsonName}": ${parameter.value}"""
|
||||
def formatParameterAsHumanReadableString(
|
||||
parameter: VersionDescriptionParameter
|
||||
): String =
|
||||
s"${parameter.humanReadableName}: ${parameter.humandReadableValue}"
|
||||
s"${parameter.humanReadableName}: ${parameter.value}"
|
||||
|
||||
def make(
|
||||
header: String,
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user