mirror of
https://github.com/enso-org/enso.git
synced 2024-09-11 21:27:15 +03:00
Implement Launcher Self-Update (#1125)
This commit is contained in:
parent
6301542546
commit
044a0fa664
19
.github/workflows/release.yml
vendored
19
.github/workflows/release.yml
vendored
@ -86,6 +86,8 @@ jobs:
|
||||
restore-keys: ${{ runner.os }}-sbt-
|
||||
|
||||
# Build Artifacts
|
||||
- name: Enable Release Mode
|
||||
run: echo ::set-env name=ENSO_RELEASE_MODE::true
|
||||
- name: Bootstrap the Project
|
||||
working-directory: repo
|
||||
run: |
|
||||
@ -111,12 +113,13 @@ jobs:
|
||||
run: |
|
||||
sleep 1
|
||||
sbt --no-colors launcher/buildNativeImage
|
||||
- name: Build the Manifest
|
||||
- name: Build the Manifests
|
||||
working-directory: repo
|
||||
run: |
|
||||
cp distribution/manifest.template.yaml manifest.yaml
|
||||
echo "graal-vm-version: ${{ env.graalVersion }}" >> manifest.yaml
|
||||
echo "graal-java-version: ${{ env.javaVersion }}" >> manifest.yaml
|
||||
cp distribution/launcher-manifest.yaml launcher-manifest.yaml
|
||||
|
||||
# Prepare distributions
|
||||
- name: Prepare Distribution Version (Unix)
|
||||
@ -240,6 +243,11 @@ jobs:
|
||||
with:
|
||||
name: manifest
|
||||
path: repo/manifest.yaml
|
||||
- name: Upload the Launcher Manifest Artifact
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: launcher-manifest
|
||||
path: repo/launcher-manifest.yaml
|
||||
|
||||
create-release:
|
||||
name: Prepare Release
|
||||
@ -444,3 +452,12 @@ jobs:
|
||||
asset_path: artifacts/manifest/manifest.yaml
|
||||
asset_name: manifest.yaml
|
||||
asset_content_type: application/yaml
|
||||
- name: Publish the Launcher Manifest
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: artifacts/launcher-manifest/launcher-manifest.yaml
|
||||
asset_name: launcher-manifest.yaml
|
||||
asset_content_type: application/yaml
|
||||
|
@ -13,6 +13,7 @@ members = [
|
||||
"lib/rust/flexer",
|
||||
"lib/rust/flexer-testing/definition",
|
||||
"lib/rust/flexer-testing/generation",
|
||||
"lib/rust/launcher-shims",
|
||||
"lib/rust/lazy-reader",
|
||||
"lib/rust/lexer/definition",
|
||||
"lib/rust/lexer/generation",
|
||||
|
@ -1061,6 +1061,9 @@ lazy val launcher = project
|
||||
"enso"
|
||||
)
|
||||
)
|
||||
.dependsOn(
|
||||
LauncherShimsForTest.prepare(rustcVersion = rustVersion)
|
||||
)
|
||||
.value
|
||||
)
|
||||
.settings(licenseSettings)
|
||||
|
6
distribution/launcher-manifest.yaml
Normal file
6
distribution/launcher-manifest.yaml
Normal file
@ -0,0 +1,6 @@
|
||||
minimum-version-for-upgrade: 0.1.0
|
||||
files-to-copy:
|
||||
- NOTICE
|
||||
- README.md
|
||||
directories-to-copy:
|
||||
- components-licences
|
@ -218,9 +218,11 @@ Examples:
|
||||
|
||||
```bash
|
||||
> enso upgrade
|
||||
Launcher has been upgraded to the latest (3.0.2) version.
|
||||
...
|
||||
[info] Successfully upgraded launcher to 3.0.2.
|
||||
> enso upgrade 2.0.1
|
||||
Launcher has been downgraded to version 2.0.1.
|
||||
...
|
||||
[info] Successfully upgraded launcher to 2.0.1.
|
||||
```
|
||||
|
||||
### `version`
|
||||
@ -234,17 +236,17 @@ human-readable format that is the default.
|
||||
```bash
|
||||
> enso version
|
||||
Enso Launcher
|
||||
Version: 0.0.1
|
||||
Built with: scala-2.13.3 for GraalVM 20.1.0
|
||||
Built from: main @ 919ffbdfacc44cc35a1b38f1bad5b573acdbe358
|
||||
Running on: Linux 4.15.0-109-generic (amd64)
|
||||
Currently selected Enso version:
|
||||
Version: 0.1.0
|
||||
Built with: scala-2.13.3 for GraalVM 20.2.0
|
||||
Built from: wip/rw/launcher-self-update* @ c76f7fe6a9e9f37cd8a296c615b7515d1b896d73
|
||||
Built on: Linux (amd64)
|
||||
Current default Enso engine:
|
||||
Enso Compiler and Runtime
|
||||
Version: 0.0.1
|
||||
Version: 0.1.1-rc5
|
||||
Built with: scala-2.13.3 for GraalVM 20.1.0
|
||||
Built from: main @ 919ffbdfacc44cc35a1b38f1bad5b573acdbe358
|
||||
Built from: enso-0.1.1-rc5 @ 391eca6de06b0c642cf7868db62209a9af3d241d
|
||||
Running on: OpenJDK 64-Bit Server VM, GraalVM Community, JDK 11.0.7+10-jvmci-20.1-b02
|
||||
Linux 4.15.0-108-generic (amd64)
|
||||
Linux 4.15.0-112-generic (amd64)
|
||||
```
|
||||
|
||||
Besides `enso version`, `enso --version` is also supported and yields the same
|
||||
|
@ -32,6 +32,7 @@ command-line interface is described in the [CLI](./launcher-cli.md) document.
|
||||
- [Global User Configuration](#global-user-configuration)
|
||||
- [Updating the Launcher](#updating-the-launcher)
|
||||
- [Minimal Required Launcher Version](#minimal-required-launcher-version)
|
||||
- [Step-by-Step Upgrade](#step-by-step-upgrade)
|
||||
- [Downloading Launcher Releases](#downloading-launcher-releases)
|
||||
|
||||
<!-- /MarkdownTOC -->
|
||||
@ -105,11 +106,6 @@ the default version.
|
||||
> the launcher check for new versions) and resolvers.
|
||||
> - Decide how to support nightly builds.
|
||||
|
||||
### Project Configuration
|
||||
|
||||
The command-line allows to edit project configuration, for example: change the
|
||||
author's name or Enso version.
|
||||
|
||||
## Enso and Graal Version Management
|
||||
|
||||
The launcher automatically manages required Enso versions. When running inside a
|
||||
@ -132,10 +128,6 @@ right JVM version is used to launch each version of Enso, the user can override
|
||||
this mechanism to use the installed system JVM instead. This is an advanced
|
||||
feature and should rarely be used.
|
||||
|
||||
The launcher will check the system JVM and refuse to launch Enso if it is not a
|
||||
GraalVM distribution. It will also print a warning if the major or minor version
|
||||
is different then required by that particular Enso version.
|
||||
|
||||
### Downloading Enso Releases
|
||||
|
||||
The releases are discovered and downloaded using the
|
||||
@ -239,8 +231,40 @@ newer version of the launcher. Thus, project configuration can also specify a
|
||||
minimal required launcher version.
|
||||
|
||||
If the launcher detects that the installed version is older than one of the two
|
||||
criteria above, it asks the user to upgrade the launcher using the `upgrade`
|
||||
command.
|
||||
criteria above, it offers to automatically upgrade to the latest version and
|
||||
re-run the current command.
|
||||
|
||||
### Step-by-Step Upgrade
|
||||
|
||||
It is possible that in the future, new launcher versions will require some
|
||||
additional logic when upgrading that has not currently been considered. To
|
||||
maintain future-compatibility, each launcher version can define in its manifest
|
||||
a minimum launcher version that can be used to upgrade to it. Any new upgrade
|
||||
logic can then be introduced gradually (by first releasing a new version which
|
||||
knows this new logic but does not require it and later releasing another version
|
||||
that can require this new logic). In that case, updates are performed
|
||||
step-by-step - first this new version that does not require new logic is
|
||||
downloaded and it is used to upgrade to the new version which requires the new
|
||||
logic. If necessary, such upgrade steps can be chained.
|
||||
|
||||
The step-by-step upgrade logic is implemented by checking the
|
||||
`minimum-version-for-upgrade` property in the new launcher's manifest. If the
|
||||
current version is greater or equal to that version, the upgrade proceeds
|
||||
normally. Otherwise, the launcher tries to upgrade to this minimum version (or
|
||||
the closest to it newer non-broken version), recursively - i.e. if this upgrade
|
||||
also cannot be performed directly, it is also performed step-by-step in the same
|
||||
way.
|
||||
|
||||
#### Testing Step-by-Step Upgrade
|
||||
|
||||
To test the multi-step upgrade we need multiple launcher executables that report
|
||||
different versions. As building the launcher takes a substantial amount of time
|
||||
and it reports by default the version from build information, we created a
|
||||
simple wrapper in Rust which runs the original launcher executable with
|
||||
additional internal options that tell it to override its version. These options
|
||||
are only available in a development build. Similarly, internal options are used
|
||||
to override the default GitHub repository to a local filesystem based repository
|
||||
for launcher releases, as we want to avoid any network connectivity in tests.
|
||||
|
||||
### Downloading Launcher Releases
|
||||
|
||||
|
@ -18,6 +18,8 @@ that we have a well-defined release policy. This document defines said policy.
|
||||
- [Release Branches](#release-branches)
|
||||
- [Release Workflow](#release-workflow)
|
||||
- [Tag Naming](#tag-naming)
|
||||
- [Manifest Files](#manifest-files)
|
||||
- [Breaking Changes to Launcher Upgrade](#breaking-changes-to-launcher-upgrade)
|
||||
- [GitHub Releases](#github-releases)
|
||||
- [Release Notes](#release-notes)
|
||||
- [Version Support](#version-support)
|
||||
@ -86,13 +88,9 @@ Tags for releases are named as follows `enso-version`, where `version` is the
|
||||
semver string (see [versioning](#versioning)) representing the version being
|
||||
released.
|
||||
|
||||
### GitHub Releases
|
||||
### Manifest Files
|
||||
|
||||
A release is considered _official_ once it has been made into a release on
|
||||
[GitHub](https://github.com/enso-org/enso/releases). Once official, a release
|
||||
may not be changed in any way, except to mark it as broken.
|
||||
|
||||
#### Manifest File
|
||||
#### Engine Manifest
|
||||
|
||||
Each GitHub release contains an asset named `manifest.yaml` which is a YAML file
|
||||
containing metadata regarding the release. The manifest is also included in the
|
||||
@ -142,6 +140,58 @@ stored in
|
||||
[`distribution/manifest.template.yaml`](../../distribution/manifest.template.yaml)
|
||||
and other values are added to this template at build time.
|
||||
|
||||
#### Launcher Manifest
|
||||
|
||||
Additionally, each release should contain an asset named
|
||||
`launcher-manifest.yaml` which contains launcher-specific release metadata.
|
||||
|
||||
It contains the following fields:
|
||||
|
||||
- `minimum-version-for-upgrade` - specifies the minimum version of the launcher
|
||||
that is allowed to upgrade to this launcher version. If a launcher is older
|
||||
than the version specified here it must perform the upgrade in steps, first
|
||||
upgrading to an older version newer than `minimum-version-for-upgrade` and
|
||||
only then, using that version, to the target version. This logic ensures that
|
||||
if a newer launcher version required custom upgrade logic not present in older
|
||||
versions, the upgrade can still be performed by first upgrading to a newer
|
||||
version that does not require the new logic but knows about it and continuing
|
||||
the upgrade with that knowledge.
|
||||
- `files-to-copy` - a list of files that should be copied into the
|
||||
distribution's data root. This may include the `README` and similar files, so
|
||||
that after the upgrade these additional files are also up-to-date. These files
|
||||
are treated as non-essential, i.e. an error when copying them will not cancel
|
||||
the upgrade (but it should be reported).
|
||||
- `directories-to-copy` - a list of directories that should be copied into the
|
||||
distribution's data root. Acts similarly to `files-to-copy`.
|
||||
|
||||
A template manifest file, located in
|
||||
[`distribution/launcher-manifest.yaml`](../../distribution/launcher-manifest.yaml),
|
||||
is automatically copied to the release. If any new files or directories are
|
||||
added or a breaking change to the upgrade mechanism is being made, this manifest
|
||||
template must be updated accordingly.
|
||||
|
||||
### Breaking Changes to Launcher Upgrade
|
||||
|
||||
If at any point the launcher's upgrade mechanism needs an update, i.e.
|
||||
additional logic must be added that was not present before, special action is
|
||||
required.
|
||||
|
||||
First, the additional logic has to be implemented and a new launcher version
|
||||
should be released which includes this additional logic, but does not require it
|
||||
yet. Then, another version can be released that can depend on this new logic and
|
||||
its `minimum-version-for-upgrade` has to be bumped to that previous version
|
||||
which already includes new logic but does not depend on it.
|
||||
|
||||
This way, old launcher versions can first upgrade to a version that contains the
|
||||
new logic (as it does not depend on it yet, the upgrade is possible) and using
|
||||
that new version, upgrade to the target version that depends on that logic.
|
||||
|
||||
### GitHub Releases
|
||||
|
||||
A release is considered _official_ once it has been made into a release on
|
||||
[GitHub](https://github.com/enso-org/enso/releases). Once official, a release
|
||||
may not be changed in any way, except to mark it as broken.
|
||||
|
||||
#### Release Assets Structure
|
||||
|
||||
Each release contains a build of the Enso engine and native launcher binaries
|
||||
|
@ -504,10 +504,6 @@ Enso follows the standard rust convention for structuring crates, as provided by
|
||||
`cargo new`. This is discussed more in depth
|
||||
[here](https://learning-rust.github.io/docs/a4.cargo,crates_and_basic_project_structure.html#Project-Structure).
|
||||
|
||||
In order to match up with the project naming convention we use for Scala and
|
||||
Java projects, any rust code must be in a directory named using `UpperCamelCase`
|
||||
in the root of the project (e.g. `enso/BaseGL`).
|
||||
|
||||
### The Public API
|
||||
|
||||
Whereas Rust defaults to making module members _private_ by default, this is not
|
||||
|
@ -0,0 +1,40 @@
|
||||
package org.enso.launcher
|
||||
|
||||
import buildinfo.Info
|
||||
import nl.gn0s1s.bump.SemVer
|
||||
|
||||
/**
|
||||
* Helper object that allows to get the current launcher version.
|
||||
*
|
||||
* In development-mode it allows to override the returned version for testing
|
||||
* purposes.
|
||||
*/
|
||||
object CurrentVersion {
|
||||
|
||||
private var currentVersion: SemVer = SemVer(Info.ensoVersion).getOrElse {
|
||||
throw new IllegalStateException("Cannot parse the built-in version.")
|
||||
}
|
||||
|
||||
/**
|
||||
* Version of the launcher.
|
||||
*/
|
||||
def version: SemVer = currentVersion
|
||||
|
||||
/**
|
||||
* Override launcher version with the provided one.
|
||||
*
|
||||
* Internal helper method used for testing. It should be called before any
|
||||
* calls to [[version]].
|
||||
*/
|
||||
def internalOverrideVersion(newVersion: SemVer): Unit =
|
||||
if (Info.isRelease)
|
||||
throw new IllegalStateException(
|
||||
"Internal testing function internalOverrideVersion used in a " +
|
||||
"release build."
|
||||
)
|
||||
else {
|
||||
Logger.debug(s"[TEST] Overriding version to $newVersion.")
|
||||
currentVersion = newVersion
|
||||
}
|
||||
|
||||
}
|
@ -138,7 +138,21 @@ trait Environment {
|
||||
* returns a path to the root of the classpath for the `org.enso.launcher`
|
||||
* package or a built JAR.
|
||||
*/
|
||||
def getPathToRunningExecutable: Path = {
|
||||
def getPathToRunningExecutable: Path
|
||||
}
|
||||
|
||||
/**
|
||||
* The default [[Environment]] implementation.
|
||||
*/
|
||||
object Environment extends Environment {
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
override def getPathToRunningExecutable: Path =
|
||||
executablePathOverride.getOrElse(executablePath)
|
||||
|
||||
private def executablePath: Path =
|
||||
try {
|
||||
val codeSource =
|
||||
this.getClass.getProtectionDomain.getCodeSource
|
||||
@ -150,10 +164,24 @@ trait Environment {
|
||||
e
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The default [[Environment]] implementation.
|
||||
*/
|
||||
object Environment extends Environment
|
||||
private var executablePathOverride: Option[Path] = None
|
||||
|
||||
/**
|
||||
* Overrides the return value of [[getPathToRunningExecutable]] with the
|
||||
* provided path.
|
||||
*
|
||||
* Internal method used for testing. It should be called as early as
|
||||
* possible, before [[getPathToRunningExecutable]] is called.
|
||||
*/
|
||||
def internalOverrideExecutableLocation(newLocation: Path): Unit =
|
||||
if (buildinfo.Info.isRelease)
|
||||
throw new IllegalStateException(
|
||||
"Internal testing function internalOverrideExecutableLocation used " +
|
||||
"in a release build."
|
||||
)
|
||||
else {
|
||||
Logger.debug(s"[TEST] Overriding location to $newLocation.")
|
||||
executablePathOverride = Some(newLocation)
|
||||
}
|
||||
}
|
||||
|
@ -2,11 +2,10 @@ package org.enso.launcher
|
||||
|
||||
import java.nio.file.Path
|
||||
|
||||
import buildinfo.Info
|
||||
import io.circe.Json
|
||||
import nl.gn0s1s.bump.SemVer
|
||||
import org.enso.launcher.cli.GlobalCLIOptions
|
||||
import org.enso.launcher.components.DefaultComponentsManager
|
||||
import org.enso.launcher.components.ComponentsManager
|
||||
import org.enso.launcher.components.runner.{
|
||||
JVMSettings,
|
||||
LanguageServerOptions,
|
||||
@ -16,6 +15,7 @@ import org.enso.launcher.components.runner.{
|
||||
import org.enso.launcher.config.{DefaultVersion, GlobalConfigurationManager}
|
||||
import org.enso.launcher.installation.DistributionManager
|
||||
import org.enso.launcher.project.ProjectManager
|
||||
import org.enso.launcher.upgrade.LauncherUpgrader
|
||||
import org.enso.version.{VersionDescription, VersionDescriptionParameter}
|
||||
|
||||
/**
|
||||
@ -25,7 +25,7 @@ import org.enso.version.{VersionDescription, VersionDescriptionParameter}
|
||||
* @param cliOptions the global CLI options to use for the commands
|
||||
*/
|
||||
case class Launcher(cliOptions: GlobalCLIOptions) {
|
||||
private lazy val componentsManager = DefaultComponentsManager(cliOptions)
|
||||
private lazy val componentsManager = ComponentsManager.makeDefault(cliOptions)
|
||||
private lazy val configurationManager =
|
||||
new GlobalConfigurationManager(componentsManager, DistributionManager)
|
||||
private lazy val projectManager = new ProjectManager(configurationManager)
|
||||
@ -36,6 +36,8 @@ case class Launcher(cliOptions: GlobalCLIOptions) {
|
||||
componentsManager,
|
||||
Environment
|
||||
)
|
||||
private lazy val upgrader = LauncherUpgrader.makeDefault(cliOptions)
|
||||
upgrader.runCleanup(isStartup = true)
|
||||
|
||||
/**
|
||||
* Creates a new project with the given `name` in the given `path`.
|
||||
@ -342,7 +344,8 @@ case class Launcher(cliOptions: GlobalCLIOptions) {
|
||||
"Enso Launcher",
|
||||
includeRuntimeJVMInfo = false,
|
||||
enableNativeImageOSWorkaround = true,
|
||||
additionalParameters = runtimeVersionParameter.toSeq
|
||||
additionalParameters = runtimeVersionParameter.toSeq,
|
||||
customVersion = Some(CurrentVersion.version.toString)
|
||||
)
|
||||
|
||||
println(versionDescription.asString(useJSON))
|
||||
@ -378,20 +381,40 @@ case class Launcher(cliOptions: GlobalCLIOptions) {
|
||||
value = runtimeVersionString
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a self-upgrade.
|
||||
*
|
||||
* If a `version` is specified, installs that version. If the version is
|
||||
* older than the current one, a downgrade is performed. If no `version` is
|
||||
* specified, the latest available version is chosen, unless it is older than
|
||||
* the current one.
|
||||
*/
|
||||
def upgrade(version: Option[SemVer]): Unit = {
|
||||
val targetVersion = version.getOrElse(upgrader.latestVersion().get)
|
||||
val isManuallyRequested = version.isDefined
|
||||
if (targetVersion == CurrentVersion.version) {
|
||||
Logger.info("Already up-to-date.")
|
||||
} else if (targetVersion < CurrentVersion.version && !isManuallyRequested) {
|
||||
Logger.warn(
|
||||
s"The latest available version is $targetVersion, but you are " +
|
||||
s"running ${CurrentVersion.version} which is more recent."
|
||||
)
|
||||
Logger.info(
|
||||
s"If you really want to downgrade, please run " +
|
||||
s"`enso upgrade $targetVersion`."
|
||||
)
|
||||
sys.exit(1)
|
||||
} else {
|
||||
upgrader.upgrade(targetVersion)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gathers launcher commands which do not depend on the global CLI options.
|
||||
*/
|
||||
object Launcher {
|
||||
|
||||
/**
|
||||
* Version of the launcher.
|
||||
*/
|
||||
val version: SemVer = SemVer(Info.ensoVersion).getOrElse {
|
||||
throw new IllegalStateException("Cannot parse the built-in version.")
|
||||
}
|
||||
|
||||
private val workingDirectory: Path = Path.of(".")
|
||||
|
||||
/**
|
||||
|
@ -3,7 +3,10 @@ package org.enso.launcher.archive
|
||||
import java.io.BufferedInputStream
|
||||
import java.nio.file.{Files, Path}
|
||||
|
||||
import org.apache.commons.compress.archivers.{ArchiveEntry, ArchiveInputStream}
|
||||
import org.apache.commons.compress.archivers.{
|
||||
ArchiveEntry => ApacheArchiveEntry,
|
||||
ArchiveInputStream
|
||||
}
|
||||
import org.apache.commons.compress.archivers.tar.{
|
||||
TarArchiveEntry,
|
||||
TarArchiveInputStream
|
||||
@ -49,18 +52,22 @@ object Archive {
|
||||
renameRootFolder: Option[Path]
|
||||
): TaskProgress[Unit] = {
|
||||
val format = ArchiveFormat.detect(archivePath)
|
||||
format
|
||||
.map(
|
||||
extractArchive(archivePath, _, destinationDirectory, renameRootFolder)
|
||||
)
|
||||
.getOrElse {
|
||||
format match {
|
||||
case Some(archiveFormat) =>
|
||||
extractArchive(
|
||||
archivePath,
|
||||
archiveFormat,
|
||||
destinationDirectory,
|
||||
renameRootFolder
|
||||
)
|
||||
case None =>
|
||||
TaskProgress.immediateFailure(
|
||||
new IllegalArgumentException(
|
||||
s"Could not detect archive format for " +
|
||||
s"${archivePath.getFileName}"
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -89,55 +96,129 @@ object Archive {
|
||||
format: ArchiveFormat,
|
||||
destinationDirectory: Path,
|
||||
renameRootFolder: Option[Path]
|
||||
): TaskProgress[Unit] = {
|
||||
val rewritePath: Path => Path = renameRootFolder match {
|
||||
case Some(value) => new BaseRenamer(value)
|
||||
case None => identity[Path]
|
||||
}
|
||||
|
||||
iterateArchive(archivePath, format) { entry =>
|
||||
val destinationPath =
|
||||
destinationDirectory.resolve(rewritePath(entry.relativePath))
|
||||
entry.extractTo(destinationPath)
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterates over entries of the archive at `archivePath`.
|
||||
*
|
||||
* The iteration is run in a background thread, the function returns
|
||||
* immediately with a [[TaskProgress]] instance that can be used to track
|
||||
* iteration progress (for example by displaying a progress bar or just
|
||||
* waiting for it to complete).
|
||||
*
|
||||
* Tries to detect the archive format automatically.
|
||||
*
|
||||
* @param archivePath path to the archive file
|
||||
* @return an instance indicating the progress of iteration
|
||||
*/
|
||||
def iterateArchive(archivePath: Path)(
|
||||
callback: ArchiveEntry => Boolean
|
||||
): TaskProgress[Unit] = {
|
||||
val format = ArchiveFormat.detect(archivePath)
|
||||
format match {
|
||||
case Some(archiveFormat) =>
|
||||
iterateArchive(archivePath, archiveFormat)(callback)
|
||||
case None =>
|
||||
TaskProgress.immediateFailure(
|
||||
new IllegalArgumentException(
|
||||
s"Could not detect archive format for " +
|
||||
s"${archivePath.getFileName}"
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterates over entries of the archive at `archivePath`.
|
||||
*
|
||||
* The iteration is run in a background thread, the function returns
|
||||
* immediately with a [[TaskProgress]] instance that can be used to track
|
||||
* iteration progress (for example by displaying a progress bar or just
|
||||
* waiting for it to complete).
|
||||
*
|
||||
* The callback is called for each encountered archive entry. If the callback
|
||||
* returns false, iteration is stopped.
|
||||
*
|
||||
* @param archivePath path to the archive file
|
||||
* @param format format of the archive
|
||||
* @param callback callback to call for each archive entry; if it returns
|
||||
* false, iteration is stopped
|
||||
* @return an instance indicating the progress of iteration
|
||||
*/
|
||||
def iterateArchive(archivePath: Path, format: ArchiveFormat)(
|
||||
callback: ArchiveEntry => Boolean
|
||||
): TaskProgress[Unit] = {
|
||||
val taskProgress = new TaskProgressImplementation[Unit]
|
||||
|
||||
def runExtraction(): Unit = {
|
||||
val rewritePath: Path => Path = renameRootFolder match {
|
||||
case Some(value) => new BaseRenamer(value)
|
||||
case None => identity[Path]
|
||||
}
|
||||
|
||||
Logger.debug(s"Extracting `$archivePath` to `$destinationDirectory`.")
|
||||
Logger.debug(s"Opening `$archivePath`.")
|
||||
var missingPermissions: Int = 0
|
||||
|
||||
val result = withOpenArchive(archivePath, format) { (archive, progress) =>
|
||||
for (entry <- ArchiveIterator(archive)) {
|
||||
if (!archive.canReadEntryData(entry)) {
|
||||
def processEntry(entry: ApacheArchiveEntry): Boolean = {
|
||||
val continue = if (!archive.canReadEntryData(entry)) {
|
||||
throw new RuntimeException(
|
||||
s"Cannot read ${entry.getName} from $archivePath. " +
|
||||
s"The archive may be corrupted."
|
||||
)
|
||||
} else {
|
||||
val path = parseArchiveEntryName(entry.getName)
|
||||
val destinationPath =
|
||||
destinationDirectory.resolve(rewritePath(path))
|
||||
if (entry.isDirectory) {
|
||||
Files.createDirectories(destinationPath)
|
||||
} else {
|
||||
val parent = destinationPath.getParent
|
||||
Files.createDirectories(parent)
|
||||
Using(Files.newOutputStream(destinationPath)) { out =>
|
||||
IOUtils.copy(archive, out)
|
||||
val decoratedEntry = new ArchiveEntry {
|
||||
override def isDirectory: Boolean = entry.isDirectory
|
||||
override def relativePath: Path = path
|
||||
override def extractTo(destinationPath: Path): Unit = {
|
||||
if (entry.isDirectory) {
|
||||
Files.createDirectories(destinationPath)
|
||||
} else {
|
||||
val parent = destinationPath.getParent
|
||||
Files.createDirectories(parent)
|
||||
Using(Files.newOutputStream(destinationPath)) { out =>
|
||||
IOUtils.copy(archive, out)
|
||||
}
|
||||
}
|
||||
|
||||
if (OS.isUNIX) {
|
||||
getMode(entry) match {
|
||||
case Some(mode) =>
|
||||
val permissions = FileSystem.decodePOSIXPermissions(mode)
|
||||
Files.setPosixFilePermissions(
|
||||
destinationPath,
|
||||
permissions
|
||||
)
|
||||
case None =>
|
||||
missingPermissions += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (OS.isUNIX) {
|
||||
getMode(entry) match {
|
||||
case Some(mode) =>
|
||||
val permissions = FileSystem.decodePOSIXPermissions(mode)
|
||||
Files.setPosixFilePermissions(destinationPath, permissions)
|
||||
case None =>
|
||||
missingPermissions += 1
|
||||
}
|
||||
}
|
||||
callback(decoratedEntry)
|
||||
}
|
||||
|
||||
taskProgress.reportProgress(
|
||||
progress.alreadyRead(),
|
||||
progress.total()
|
||||
)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
val iterator = ArchiveIterator(archive)
|
||||
val _ = iterator.takeWhile(processEntry).length
|
||||
|
||||
()
|
||||
}
|
||||
|
||||
if (missingPermissions > 0) {
|
||||
@ -150,7 +231,7 @@ object Archive {
|
||||
|
||||
taskProgress.setComplete(result)
|
||||
}
|
||||
val thread = new Thread(() => runExtraction(), "Extracting-Archive")
|
||||
val thread = new Thread(() => runExtraction(), "Reading-Archive")
|
||||
thread.start()
|
||||
taskProgress
|
||||
}
|
||||
@ -158,7 +239,7 @@ object Archive {
|
||||
/**
|
||||
* Tries to get the POSIX file permissions associated with that `entry`.
|
||||
*/
|
||||
private def getMode(entry: ArchiveEntry): Option[Int] =
|
||||
private def getMode(entry: ApacheArchiveEntry): Option[Int] =
|
||||
entry match {
|
||||
case entry: TarArchiveEntry =>
|
||||
Some(entry.getMode)
|
||||
|
@ -0,0 +1,32 @@
|
||||
package org.enso.launcher.archive
|
||||
|
||||
import java.nio.file.Path
|
||||
|
||||
/**
|
||||
* An archive entry that is provided to the callback when iterating over an
|
||||
* archive.
|
||||
*/
|
||||
trait ArchiveEntry {
|
||||
|
||||
/**
|
||||
* Specifies if the entry represents a directory.
|
||||
*
|
||||
* If false, it represents a normal file.
|
||||
*/
|
||||
def isDirectory: Boolean
|
||||
|
||||
/**
|
||||
* Relative path of the entry inside of the archive.
|
||||
*/
|
||||
def relativePath: Path
|
||||
|
||||
/**
|
||||
* Extracts the entry to the provided destination.
|
||||
*
|
||||
* The destination specifies the full path to the extracted entry including
|
||||
* its new filename, not just a parent directory. On UNIX platforms, the
|
||||
* permissions of the extracted entry are set as specified in the archive, if
|
||||
* possible.
|
||||
*/
|
||||
def extractTo(destination: Path): Unit
|
||||
}
|
@ -16,3 +16,21 @@ case class GlobalCLIOptions(
|
||||
hideProgress: Boolean,
|
||||
useJSON: Boolean
|
||||
)
|
||||
|
||||
object GlobalCLIOptions {
|
||||
val HIDE_PROGRESS = "hide-progress"
|
||||
val AUTO_CONFIRM = "auto-confirm"
|
||||
val USE_JSON = "json"
|
||||
|
||||
/**
|
||||
* Converts the [[GlobalCLIOptions]] to a sequence of arguments that can be
|
||||
* added to a launcher invocation to set the same options.
|
||||
*/
|
||||
def toOptions(config: GlobalCLIOptions): Seq[String] = {
|
||||
val autoConfirm = if (config.autoConfirm) Seq(s"--$AUTO_CONFIRM") else Seq()
|
||||
val hideProgress =
|
||||
if (config.hideProgress) Seq(s"--$HIDE_PROGRESS") else Seq()
|
||||
val useJSON = if (config.useJSON) Seq(s"--$USE_JSON") else Seq()
|
||||
autoConfirm ++ hideProgress ++ useJSON
|
||||
}
|
||||
}
|
||||
|
@ -4,10 +4,14 @@ import java.io.IOException
|
||||
import java.nio.file.{Files, NoSuchFileException, Path}
|
||||
|
||||
import cats.implicits._
|
||||
import nl.gn0s1s.bump.SemVer
|
||||
import org.enso.cli.arguments.Opts
|
||||
import org.enso.cli.arguments.Opts.implicits._
|
||||
import org.enso.launcher.FileSystem.PathSyntax
|
||||
import org.enso.launcher.{FileSystem, OS}
|
||||
import org.enso.launcher.cli.Arguments._
|
||||
import org.enso.launcher.releases.EnsoRepository
|
||||
import org.enso.launcher.upgrade.LauncherUpgrader
|
||||
import org.enso.launcher.{CurrentVersion, Environment, FileSystem, OS}
|
||||
|
||||
/**
|
||||
* Implements internal options that the launcher may use when running another
|
||||
@ -17,7 +21,7 @@ import org.enso.launcher.{FileSystem, OS}
|
||||
* Windows-specific filesystem limitations. They should not be used by the
|
||||
* users directly, so they are not displayed in the help text.
|
||||
*
|
||||
* The implemented workarounds are following:
|
||||
* The implemented features are following:
|
||||
*
|
||||
* 1. Remove Old Executable
|
||||
* On Windows, if an executable is running, its file is locked, so it is
|
||||
@ -36,11 +40,72 @@ import org.enso.launcher.{FileSystem, OS}
|
||||
* when the system removes temporary files) and uses it to remove the
|
||||
* original binary. It then can also remove the (now empty) installation
|
||||
* directory if necessary.
|
||||
* 3. Continue Upgrade
|
||||
* This one is not a Windows-specific workaround but a way for the launcher
|
||||
* to perform multi-step upgrade. If the launcher cannot upgrade directly to
|
||||
* a newer version (because, for example, it requires some additional
|
||||
* upgrade logic), it will first download some version 'in the middle' that
|
||||
* is old enough that it can upgrade to it directly, but new enough that it
|
||||
* has some new upgrade logic. It will extract it and run this temporary
|
||||
* launcher executable, telling it to continue the upgrade. This can be
|
||||
* repeated multiple times if multiple steps are required to reach the
|
||||
* target version. InternalOpts are used to run the new executable with all
|
||||
* the necessary options and to handle the upgrade continuation request.
|
||||
* 4. Version and Repository Emulation
|
||||
* To be able to test the upgrade mechanism, we need to run it as built
|
||||
* native executables. But we do not want to do any network requests inside
|
||||
* of the tests because of their instability. Instead, we add internal
|
||||
* options that allow to override the default, network-backed repository
|
||||
* with a fake repository backed by the filesystem. Moreover, to avoid
|
||||
* building multiple fat launcher executables, we provide an internal option
|
||||
* to override its version (and executable location). This option is used by
|
||||
* thin wrappers written in Rust which run the original launcher but
|
||||
* overriding its version. This mechanism is used for testing the multi-step
|
||||
* upgrade. These emulation options are only enabled in development builds.
|
||||
*/
|
||||
object InternalOpts {
|
||||
private val REMOVE_OLD_EXECUTABLE = "internal-remove-old-executable"
|
||||
private val FINISH_UNINSTALL = "internal-finish-uninstall"
|
||||
private val FINISH_UNINSTALL_PARENT = "internal-finish-uninstall-parent"
|
||||
private val CONTINUE_UPGRADE = "internal-continue-upgrade"
|
||||
private val UPGRADE_ORIGINAL_PATH = "internal-upgrade-original-path"
|
||||
|
||||
private val EMULATE_VERSION = "internal-emulate-version"
|
||||
private val EMULATE_LOCATION = "internal-emulate-location"
|
||||
private val EMULATE_REPOSITORY = "internal-emulate-repository"
|
||||
|
||||
private var inheritEmulateRepository: Option[Path] = None
|
||||
|
||||
/**
|
||||
* Removes internal testing options that should not be preserved in the called executable.
|
||||
*
|
||||
* In release mode, this is an identity function, since these internal options are not permitted anyway.
|
||||
*/
|
||||
def removeInternalTestOptions(args: Seq[String]): Seq[String] =
|
||||
if (buildinfo.Info.isRelease) args
|
||||
else {
|
||||
(removeOption(EMULATE_VERSION) andThen removeOption(EMULATE_LOCATION))(
|
||||
args
|
||||
)
|
||||
}
|
||||
|
||||
private def removeOption(name: String): Seq[String] => Seq[String] = {
|
||||
args: Seq[String] =>
|
||||
val indexedArgs = args.zipWithIndex
|
||||
def dropArguments(fromIdx: Int, howMany: Int = 1): Seq[String] =
|
||||
args.take(fromIdx) ++ args.drop(fromIdx + howMany)
|
||||
indexedArgs.find(_._1.startsWith(s"--$name=")) match {
|
||||
case Some((_, idx)) =>
|
||||
dropArguments(idx)
|
||||
case None =>
|
||||
indexedArgs.find(_._1 == s"--$name") match {
|
||||
case Some((_, idx)) =>
|
||||
dropArguments(idx, howMany = 2)
|
||||
case None =>
|
||||
args
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Additional top level options that are internal to the launcher and should
|
||||
@ -48,7 +113,7 @@ object InternalOpts {
|
||||
*
|
||||
* They are used to implement workarounds for install / upgrade on Windows.
|
||||
*/
|
||||
def topLevelOptions: Opts[Unit] = {
|
||||
def topLevelOptions: Opts[GlobalCLIOptions => Unit] = {
|
||||
val removeOldExecutableOpt = Opts
|
||||
.optionalParameter[Path](
|
||||
REMOVE_OLD_EXECUTABLE,
|
||||
@ -75,12 +140,34 @@ object InternalOpts {
|
||||
)
|
||||
.hidden
|
||||
|
||||
val continueUpgrade = Opts
|
||||
.optionalParameter[SemVer](
|
||||
CONTINUE_UPGRADE,
|
||||
"VERSION",
|
||||
"Executes next step of the upgrade that should finally result in " +
|
||||
"upgrading to the provided VERSION."
|
||||
)
|
||||
.hidden
|
||||
|
||||
val originalPath =
|
||||
Opts.optionalParameter[Path](UPGRADE_ORIGINAL_PATH, "PATH", "").hidden
|
||||
|
||||
(
|
||||
testingOptions,
|
||||
removeOldExecutableOpt,
|
||||
finishUninstallOpt,
|
||||
finishUninstallParentOpt
|
||||
finishUninstallParentOpt,
|
||||
continueUpgrade,
|
||||
originalPath
|
||||
) mapN {
|
||||
(removeOldExecutableOpt, finishUninstallOpt, finishUninstallParentOpt) =>
|
||||
(
|
||||
_,
|
||||
removeOldExecutableOpt,
|
||||
finishUninstallOpt,
|
||||
finishUninstallParentOpt,
|
||||
continueUpgrade,
|
||||
originalPath
|
||||
) => (config: GlobalCLIOptions) =>
|
||||
removeOldExecutableOpt.foreach { oldExecutablePath =>
|
||||
removeOldExecutable(oldExecutablePath)
|
||||
sys.exit(0)
|
||||
@ -90,9 +177,56 @@ object InternalOpts {
|
||||
finishUninstall(executablePath, finishUninstallParentOpt)
|
||||
sys.exit(0)
|
||||
}
|
||||
|
||||
continueUpgrade.foreach { version =>
|
||||
LauncherUpgrader
|
||||
.makeDefault(config, originalExecutablePath = originalPath)
|
||||
.internalContinueUpgrade(version)
|
||||
sys.exit(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal options used for testing.
|
||||
*
|
||||
* Disabled in release mode.
|
||||
*/
|
||||
private def testingOptions: Opts[Unit] =
|
||||
if (buildinfo.Info.isRelease) Opts.pure(())
|
||||
else {
|
||||
val emulateVersion =
|
||||
Opts.optionalParameter[SemVer](EMULATE_VERSION, "VERSION", "").hidden
|
||||
val emulateLocation =
|
||||
Opts.optionalParameter[Path](EMULATE_LOCATION, "PATH", "").hidden
|
||||
val emulateRepository =
|
||||
Opts.optionalParameter[Path](EMULATE_REPOSITORY, "PATH", "").hidden
|
||||
|
||||
(emulateVersion, emulateLocation, emulateRepository) mapN {
|
||||
(emulateVersion, emulateLocation, emulateRepository) =>
|
||||
emulateVersion.foreach { version =>
|
||||
CurrentVersion.internalOverrideVersion(version)
|
||||
}
|
||||
|
||||
emulateLocation.foreach { location =>
|
||||
Environment.internalOverrideExecutableLocation(location)
|
||||
}
|
||||
|
||||
emulateRepository.foreach { repositoryPath =>
|
||||
inheritEmulateRepository = Some(repositoryPath)
|
||||
EnsoRepository.internalUseFakeRepository(repositoryPath)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private def optionsToInherit: Seq[String] =
|
||||
inheritEmulateRepository
|
||||
.map { path =>
|
||||
Seq(s"--$EMULATE_REPOSITORY", path.toAbsolutePath.toString)
|
||||
}
|
||||
.getOrElse(Seq())
|
||||
|
||||
/**
|
||||
* Returns a helper class that allows to run the launcher located at the
|
||||
* provided path invoking the internal options.
|
||||
@ -148,6 +282,35 @@ object InternalOpts {
|
||||
) ++ parentParam.getOrElse(Seq())
|
||||
runDetachedAndExit(command)
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells the launcher to continue a multi-step upgrade.
|
||||
*
|
||||
* Creates an instance of [[LauncherUpgrader]] and invokes
|
||||
* [[LauncherUpgrader.internalContinueUpgrade]].
|
||||
*
|
||||
* @param targetVersion target version to upgrade to
|
||||
* @param originalPath path to the original launcher executable that should
|
||||
* be replaced with the final executable
|
||||
* @param globalCLIOptions cli options that should be inherited
|
||||
* @return exit code of the child process
|
||||
*/
|
||||
def continueUpgrade(
|
||||
targetVersion: SemVer,
|
||||
originalPath: Path,
|
||||
globalCLIOptions: GlobalCLIOptions
|
||||
): Int = {
|
||||
val inheritOpts =
|
||||
GlobalCLIOptions.toOptions(globalCLIOptions) ++ optionsToInherit
|
||||
val command = Seq(
|
||||
pathToNewLauncher.toAbsolutePath.toString,
|
||||
s"--$CONTINUE_UPGRADE",
|
||||
targetVersion.toString,
|
||||
s"--$UPGRADE_ORIGINAL_PATH",
|
||||
originalPath.toAbsolutePath.normalize.toString
|
||||
) ++ inheritOpts
|
||||
runAndWaitForResult(command)
|
||||
}
|
||||
}
|
||||
|
||||
private val retryBaseAmount = 30
|
||||
@ -206,6 +369,12 @@ object InternalOpts {
|
||||
}
|
||||
}
|
||||
|
||||
private def runAndWaitForResult(command: Seq[String]): Int = {
|
||||
val pb = new java.lang.ProcessBuilder(command: _*)
|
||||
pb.inheritIO()
|
||||
pb.start().waitFor()
|
||||
}
|
||||
|
||||
private def runDetachedAndExit(command: Seq[String]): Nothing = {
|
||||
if (!OS.isWindows) {
|
||||
throw new IllegalStateException(
|
||||
@ -215,7 +384,7 @@ object InternalOpts {
|
||||
}
|
||||
|
||||
val pb = new java.lang.ProcessBuilder(command: _*)
|
||||
pb.redirectOutput(java.lang.ProcessBuilder.Redirect.INHERIT)
|
||||
pb.inheritIO()
|
||||
pb.start()
|
||||
sys.exit()
|
||||
}
|
||||
|
@ -0,0 +1,521 @@
|
||||
package org.enso.launcher.cli
|
||||
|
||||
import java.nio.file.Path
|
||||
import java.util.UUID
|
||||
|
||||
import cats.data.NonEmptyList
|
||||
import cats.implicits._
|
||||
import nl.gn0s1s.bump.SemVer
|
||||
import org.enso.cli._
|
||||
import org.enso.cli.arguments.Opts.implicits._
|
||||
import org.enso.cli.arguments._
|
||||
import org.enso.launcher.Launcher
|
||||
import org.enso.launcher.cli.Arguments._
|
||||
import org.enso.launcher.components.runner.LanguageServerOptions
|
||||
import org.enso.launcher.config.DefaultVersion
|
||||
import org.enso.launcher.installation.DistributionInstaller.BundleAction
|
||||
import org.enso.launcher.installation.{
|
||||
DistributionInstaller,
|
||||
DistributionManager,
|
||||
DistributionUninstaller
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines the CLI commands and options for the program.
|
||||
*/
|
||||
object LauncherApplication {
|
||||
type Config = GlobalCLIOptions
|
||||
|
||||
private def versionCommand: Command[Config => Unit] =
|
||||
Command(
|
||||
"version",
|
||||
"Print version of the launcher and currently selected Enso distribution."
|
||||
) {
|
||||
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
|
||||
)
|
||||
onlyLauncherFlag map { onlyLauncher => (config: Config) =>
|
||||
Launcher(config).displayVersion(
|
||||
hideEngineVersion = onlyLauncher
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private def newCommand: Command[Config => Unit] =
|
||||
Command("new", "Create a new Enso project.", related = Seq("create")) {
|
||||
val nameOpt = Opts.positionalArgument[String]("PROJECT-NAME")
|
||||
val pathOpt = Opts.optionalArgument[Path](
|
||||
"PATH",
|
||||
"PATH specifies where to create the project. If it is not specified, " +
|
||||
"a directory called PROJECT-NAME is created in the current directory."
|
||||
)
|
||||
val additionalArgs = Opts.additionalArguments()
|
||||
|
||||
(
|
||||
nameOpt,
|
||||
pathOpt,
|
||||
versionOverride,
|
||||
systemJVMOverride,
|
||||
jvmOpts,
|
||||
additionalArgs
|
||||
) mapN {
|
||||
(
|
||||
name,
|
||||
path,
|
||||
versionOverride,
|
||||
systemJVMOverride,
|
||||
jvmOpts,
|
||||
additionalArgs
|
||||
) => (config: Config) =>
|
||||
Launcher(config).newProject(
|
||||
name = name,
|
||||
path = path,
|
||||
versionOverride = versionOverride,
|
||||
useSystemJVM = systemJVMOverride,
|
||||
jvmOpts = jvmOpts,
|
||||
additionalArguments = additionalArgs
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
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",
|
||||
"Override the Enso version that would normally be used."
|
||||
)
|
||||
|
||||
private def runCommand: Command[Config => Unit] =
|
||||
Command(
|
||||
"run",
|
||||
"Run an Enso project or script. " +
|
||||
"If `auto-confirm` is set, this will install missing engines or " +
|
||||
"runtimes without asking.",
|
||||
related = Seq("exec", "execute", "build")
|
||||
) {
|
||||
val pathOpt = Opts.optionalArgument[Path](
|
||||
"PATH",
|
||||
"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,
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private def languageServerCommand: Command[Config => Unit] =
|
||||
Command(
|
||||
"language-server",
|
||||
"Launch the Language Server for a given project. " +
|
||||
"If `auto-confirm` is set, this will install missing engines or " +
|
||||
"runtimes without asking.",
|
||||
related = Seq("server")
|
||||
) {
|
||||
val rootId = Opts.parameter[UUID]("root-id", "UUID", "Content root id.")
|
||||
val path =
|
||||
Opts.parameter[Path]("path", "PATH", "Path to the content root.")
|
||||
val interface =
|
||||
Opts.optionalParameter[String](
|
||||
"interface",
|
||||
"INTERFACE",
|
||||
"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. Defaults to 8080."
|
||||
)
|
||||
val dataPort =
|
||||
Opts.optionalParameter[Int](
|
||||
"data-port",
|
||||
"PORT",
|
||||
"Data port for visualisation protocol. Defaults to 8081."
|
||||
)
|
||||
val additionalArgs = Opts.additionalArguments()
|
||||
(
|
||||
rootId,
|
||||
path,
|
||||
interface,
|
||||
rpcPort,
|
||||
dataPort,
|
||||
versionOverride,
|
||||
systemJVMOverride,
|
||||
jvmOpts,
|
||||
additionalArgs
|
||||
) mapN {
|
||||
(
|
||||
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. " +
|
||||
"If `auto-confirm` is set, this will install missing engines or " +
|
||||
"runtimes without asking."
|
||||
) {
|
||||
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, 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[DefaultVersion](
|
||||
"VERSION",
|
||||
"If provided, sets default version to VERSION. " +
|
||||
"Otherwise, current default is displayed. VERSION can be an Enso " +
|
||||
"version string or `latest-installed`."
|
||||
)
|
||||
version map { version => (config: Config) =>
|
||||
val launcher = Launcher(config)
|
||||
version match {
|
||||
case Some(version) =>
|
||||
launcher.setDefaultVersion(version)
|
||||
case None =>
|
||||
launcher.printDefaultVersion()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private def upgradeCommand: Command[Config => Unit] =
|
||||
Command("upgrade", "Upgrade the launcher.") {
|
||||
val version = Opts.optionalArgument[SemVer](
|
||||
"VERSION",
|
||||
"VERSION specifies which launcher version to upgrade to. " +
|
||||
"If not provided, defaults to latest version."
|
||||
)
|
||||
version map { version => (config: Config) =>
|
||||
Launcher(config).upgrade(version)
|
||||
}
|
||||
}
|
||||
|
||||
private def installEngineCommand: Command[Config => Unit] =
|
||||
Command(
|
||||
"engine",
|
||||
"Install the specified engine VERSION, defaulting to the latest if " +
|
||||
"unspecified."
|
||||
) {
|
||||
val version = Opts.optionalArgument[SemVer]("VERSION")
|
||||
version map { version => (config: Config) =>
|
||||
version match {
|
||||
case Some(value) =>
|
||||
Launcher(config).installEngine(value)
|
||||
case None =>
|
||||
Launcher(config).installLatestEngine()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private def installDistributionCommand: Command[Config => Unit] =
|
||||
Command(
|
||||
"distribution",
|
||||
"Install Enso on the system, deactivating portable mode."
|
||||
) {
|
||||
|
||||
implicit val bundleActionParser: Argument[BundleAction] = {
|
||||
case "move" => DistributionInstaller.MoveBundles.asRight
|
||||
case "copy" => DistributionInstaller.CopyBundles.asRight
|
||||
case "ignore" => DistributionInstaller.IgnoreBundles.asRight
|
||||
case other =>
|
||||
OptsParseError.left(
|
||||
s"`$other` is not a valid bundle-install-mode value. " +
|
||||
s"Possible values are: `move`, `copy`, `ignore`."
|
||||
)
|
||||
}
|
||||
val bundleAction = Opts.optionalParameter[BundleAction](
|
||||
"bundle-install-mode",
|
||||
"(move | copy | ignore)",
|
||||
"Specifies how bundled engines and runtimes should be treated. " +
|
||||
"If `auto-confirm` is set, defaults to move.",
|
||||
showInUsage = false
|
||||
)
|
||||
|
||||
val doNotRemoveOldLauncher = Opts.flag(
|
||||
"no-remove-old-launcher",
|
||||
"If `auto-confirm` is set, the default behavior is to remove the old " +
|
||||
"launcher after installing the distribution. Setting this flag may " +
|
||||
"override this behavior to keep the original launcher.",
|
||||
showInUsage = true
|
||||
)
|
||||
|
||||
(bundleAction, doNotRemoveOldLauncher) mapN {
|
||||
(bundleAction, doNotRemoveOldLauncher) => (config: Config) =>
|
||||
new DistributionInstaller(
|
||||
DistributionManager,
|
||||
config.autoConfirm,
|
||||
removeOldLauncher = !doNotRemoveOldLauncher,
|
||||
bundleActionOption =
|
||||
if (config.autoConfirm)
|
||||
Some(bundleAction.getOrElse(DistributionInstaller.MoveBundles))
|
||||
else bundleAction
|
||||
).install()
|
||||
}
|
||||
}
|
||||
|
||||
private def installCommand: Command[Config => Unit] =
|
||||
Command(
|
||||
"install",
|
||||
"Install a new version of engine or install the distribution locally."
|
||||
) {
|
||||
Opts.subcommands(installEngineCommand, installDistributionCommand)
|
||||
}
|
||||
|
||||
private def uninstallEngineCommand: Command[Config => Unit] =
|
||||
Command(
|
||||
"engine",
|
||||
"Uninstall the provided engine version. If the corresponding runtime " +
|
||||
"is not used by any remaining engine installations, it is also removed."
|
||||
) {
|
||||
val version = Opts.positionalArgument[SemVer]("VERSION")
|
||||
version map { version => (config: Config) =>
|
||||
Launcher(config).uninstallEngine(version)
|
||||
}
|
||||
}
|
||||
|
||||
private def uninstallDistributionCommand: Command[Config => Unit] =
|
||||
Command(
|
||||
"distribution",
|
||||
"Uninstall whole Enso distribution and all components managed by " +
|
||||
"it. If `auto-confirm` is set, it will not attempt to remove the " +
|
||||
"ENSO_DATA_DIRECTORY and ENSO_CONFIG_DIRECTORY if they contain any " +
|
||||
"unexpected files."
|
||||
) {
|
||||
Opts.pure(()) map { (_: Unit) => (config: Config) =>
|
||||
new DistributionUninstaller(
|
||||
DistributionManager,
|
||||
autoConfirm = config.autoConfirm
|
||||
).uninstall()
|
||||
}
|
||||
}
|
||||
|
||||
private def uninstallCommand: Command[Config => Unit] =
|
||||
Command(
|
||||
"uninstall",
|
||||
"Uninstall an Enso component."
|
||||
) {
|
||||
Opts.subcommands(uninstallEngineCommand, uninstallDistributionCommand)
|
||||
}
|
||||
|
||||
private def listCommand: Command[Config => Unit] =
|
||||
Command("list", "List installed components.") {
|
||||
sealed trait Components
|
||||
case object EnsoComponents extends Components
|
||||
case object RuntimeComponents extends Components
|
||||
implicit val argumentComponent: Argument[Components] = {
|
||||
case "engine" => EnsoComponents.asRight
|
||||
case "runtime" => RuntimeComponents.asRight
|
||||
case other =>
|
||||
OptsParseError.left(
|
||||
s"Unknown argument `$other` - expected `engine`, `runtime` " +
|
||||
"or no argument to print a general summary."
|
||||
)
|
||||
}
|
||||
|
||||
val what = Opts.optionalArgument[Components](
|
||||
"COMPONENT",
|
||||
"COMPONENT can be either `engine`, `runtime` or none. " +
|
||||
"If not specified, prints a summary of all installed components."
|
||||
)
|
||||
what map { what => (config: Config) =>
|
||||
what match {
|
||||
case Some(EnsoComponents) => Launcher(config).listEngines()
|
||||
case Some(RuntimeComponents) => Launcher(config).listRuntimes()
|
||||
case None => Launcher(config).listSummary()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private def configCommand: Command[Config => Unit] =
|
||||
Command("config", "Modify global user configuration.") {
|
||||
val key = Opts.positionalArgument[String](
|
||||
"KEY",
|
||||
"Setting KEYs `author.name` and `author.email` can be used to set a" +
|
||||
" default author and maintainer for newly created projects."
|
||||
)
|
||||
val value = Opts.optionalArgument[String](
|
||||
"VALUE",
|
||||
"Setting VALUE to an empty string removes the key from the " +
|
||||
"configuration. When a VALUE is not provided, current configured " +
|
||||
"value is printed."
|
||||
)
|
||||
(key, value) mapN { (key, value) => (config: Config) =>
|
||||
value match {
|
||||
case Some(value) => Launcher(config).updateConfig(key, value)
|
||||
case None => Launcher(config).printConfig(key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private def helpCommand: Command[Config => Unit] =
|
||||
Command("help", "Display summary of available commands.") {
|
||||
Opts.pure(()) map { _ => (_: Config) => printTopLevelHelp() }
|
||||
}
|
||||
|
||||
private def topLevelOpts: Opts[() => TopLevelBehavior[Config]] = {
|
||||
val version =
|
||||
Opts.flag("version", 'V', "Display version.", showInUsage = true)
|
||||
val json = Opts.flag(
|
||||
GlobalCLIOptions.USE_JSON,
|
||||
"Use JSON instead of plain text for version output.",
|
||||
showInUsage = false
|
||||
)
|
||||
val ensurePortable = Opts.flag(
|
||||
"ensure-portable",
|
||||
"Ensures that the launcher is run in portable mode.",
|
||||
showInUsage = false
|
||||
)
|
||||
val autoConfirm = Opts.flag(
|
||||
GlobalCLIOptions.AUTO_CONFIRM,
|
||||
"Proceeds without asking confirmation questions. Please see the " +
|
||||
"options for the specific subcommand you want to run for the defaults " +
|
||||
"used by this option.",
|
||||
showInUsage = false
|
||||
)
|
||||
val hideProgress = Opts.flag(
|
||||
GlobalCLIOptions.HIDE_PROGRESS,
|
||||
"Suppresses displaying progress bars for downloads and other long " +
|
||||
"running actions. May be needed if program output is piped.",
|
||||
showInUsage = false
|
||||
)
|
||||
val internalOpts = InternalOpts.topLevelOptions
|
||||
|
||||
(
|
||||
internalOpts,
|
||||
version,
|
||||
json,
|
||||
ensurePortable,
|
||||
autoConfirm,
|
||||
hideProgress
|
||||
) mapN {
|
||||
(
|
||||
internalOptsCallback,
|
||||
version,
|
||||
useJSON,
|
||||
shouldEnsurePortable,
|
||||
autoConfirm,
|
||||
hideProgress
|
||||
) => () =>
|
||||
if (shouldEnsurePortable) {
|
||||
Launcher.ensurePortable()
|
||||
}
|
||||
|
||||
val globalCLIOptions = GlobalCLIOptions(
|
||||
autoConfirm = autoConfirm,
|
||||
hideProgress = hideProgress,
|
||||
useJSON = useJSON
|
||||
)
|
||||
|
||||
internalOptsCallback(globalCLIOptions)
|
||||
|
||||
if (version) {
|
||||
Launcher(globalCLIOptions).displayVersion(useJSON)
|
||||
TopLevelBehavior.Halt
|
||||
} else
|
||||
TopLevelBehavior.Continue(globalCLIOptions)
|
||||
}
|
||||
}
|
||||
|
||||
val application: Application[Config] =
|
||||
Application(
|
||||
"enso",
|
||||
"Enso",
|
||||
"Enso Launcher",
|
||||
topLevelOpts,
|
||||
NonEmptyList.of(
|
||||
versionCommand,
|
||||
helpCommand,
|
||||
newCommand,
|
||||
replCommand,
|
||||
runCommand,
|
||||
languageServerCommand,
|
||||
defaultCommand,
|
||||
installCommand,
|
||||
uninstallCommand,
|
||||
upgradeCommand,
|
||||
listCommand,
|
||||
configCommand
|
||||
),
|
||||
PluginManager
|
||||
)
|
||||
|
||||
private def printTopLevelHelp(): Unit = {
|
||||
CLIOutput.println(application.renderHelp())
|
||||
}
|
||||
}
|
@ -1,544 +1,34 @@
|
||||
package org.enso.launcher.cli
|
||||
|
||||
import java.nio.file.Path
|
||||
import java.util.UUID
|
||||
|
||||
import cats.data.NonEmptyList
|
||||
import cats.implicits._
|
||||
import nl.gn0s1s.bump.SemVer
|
||||
import org.enso.cli.arguments.Opts.implicits._
|
||||
import org.enso.cli._
|
||||
import org.enso.cli.arguments.{
|
||||
Application,
|
||||
Argument,
|
||||
Command,
|
||||
Opts,
|
||||
OptsParseError,
|
||||
TopLevelBehavior
|
||||
}
|
||||
import org.enso.launcher.cli.Arguments._
|
||||
import org.enso.launcher.components.runner.LanguageServerOptions
|
||||
import org.enso.launcher.config.DefaultVersion
|
||||
import org.enso.launcher.installation.DistributionInstaller.BundleAction
|
||||
import org.enso.launcher.installation.{
|
||||
DistributionInstaller,
|
||||
DistributionManager,
|
||||
DistributionUninstaller
|
||||
}
|
||||
import org.enso.launcher.{Launcher, Logger}
|
||||
import org.enso.cli.CLIOutput
|
||||
import org.enso.launcher.Logger
|
||||
import org.enso.launcher.upgrade.LauncherUpgrader
|
||||
|
||||
/**
|
||||
* Defines the CLI commands and options for the program and its entry point.
|
||||
* Defines the entry point for the launcher.
|
||||
*/
|
||||
object Main {
|
||||
type Config = GlobalCLIOptions
|
||||
|
||||
private def versionCommand: Command[Config => Unit] =
|
||||
Command(
|
||||
"version",
|
||||
"Print version of the launcher and currently selected Enso distribution."
|
||||
) {
|
||||
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
|
||||
)
|
||||
onlyLauncherFlag map { onlyLauncher => (config: Config) =>
|
||||
Launcher(config).displayVersion(
|
||||
hideEngineVersion = onlyLauncher
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private def newCommand: Command[Config => Unit] =
|
||||
Command("new", "Create a new Enso project.", related = Seq("create")) {
|
||||
val nameOpt = Opts.positionalArgument[String]("PROJECT-NAME")
|
||||
val pathOpt = Opts.optionalArgument[Path](
|
||||
"PATH",
|
||||
"PATH specifies where to create the project. If it is not specified, " +
|
||||
"a directory called PROJECT-NAME is created in the current directory."
|
||||
)
|
||||
val additionalArgs = Opts.additionalArguments()
|
||||
|
||||
(
|
||||
nameOpt,
|
||||
pathOpt,
|
||||
versionOverride,
|
||||
systemJVMOverride,
|
||||
jvmOpts,
|
||||
additionalArgs
|
||||
) mapN {
|
||||
(
|
||||
name,
|
||||
path,
|
||||
versionOverride,
|
||||
systemJVMOverride,
|
||||
jvmOpts,
|
||||
additionalArgs
|
||||
) => (config: Config) =>
|
||||
Launcher(config).newProject(
|
||||
name = name,
|
||||
path = path,
|
||||
versionOverride = versionOverride,
|
||||
useSystemJVM = systemJVMOverride,
|
||||
jvmOpts = jvmOpts,
|
||||
additionalArguments = additionalArgs
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
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",
|
||||
"Override the Enso version that would normally be used."
|
||||
)
|
||||
|
||||
private def runCommand: Command[Config => Unit] =
|
||||
Command(
|
||||
"run",
|
||||
"Run an Enso project or script. " +
|
||||
"If `auto-confirm` is set, this will install missing engines or " +
|
||||
"runtimes without asking.",
|
||||
related = Seq("exec", "execute", "build")
|
||||
) {
|
||||
val pathOpt = Opts.optionalArgument[Path](
|
||||
"PATH",
|
||||
"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,
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private def languageServerCommand: Command[Config => Unit] =
|
||||
Command(
|
||||
"language-server",
|
||||
"Launch the Language Server for a given project. " +
|
||||
"If `auto-confirm` is set, this will install missing engines or " +
|
||||
"runtimes without asking.",
|
||||
related = Seq("server")
|
||||
) {
|
||||
val rootId = Opts.parameter[UUID]("root-id", "UUID", "Content root id.")
|
||||
val path =
|
||||
Opts.parameter[Path]("path", "PATH", "Path to the content root.")
|
||||
val interface =
|
||||
Opts.optionalParameter[String](
|
||||
"interface",
|
||||
"INTERFACE",
|
||||
"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. Defaults to 8080."
|
||||
)
|
||||
val dataPort =
|
||||
Opts.optionalParameter[Int](
|
||||
"data-port",
|
||||
"PORT",
|
||||
"Data port for visualisation protocol. Defaults to 8081."
|
||||
)
|
||||
val additionalArgs = Opts.additionalArguments()
|
||||
(
|
||||
rootId,
|
||||
path,
|
||||
interface,
|
||||
rpcPort,
|
||||
dataPort,
|
||||
versionOverride,
|
||||
systemJVMOverride,
|
||||
jvmOpts,
|
||||
additionalArgs
|
||||
) mapN {
|
||||
(
|
||||
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. " +
|
||||
"If `auto-confirm` is set, this will install missing engines or " +
|
||||
"runtimes without asking."
|
||||
) {
|
||||
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, 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[DefaultVersion](
|
||||
"VERSION",
|
||||
"If provided, sets default version to VERSION. " +
|
||||
"Otherwise, current default is displayed. VERSION can be an Enso " +
|
||||
"version string or `latest-installed`."
|
||||
)
|
||||
version map { version => (config: Config) =>
|
||||
val launcher = Launcher(config)
|
||||
version match {
|
||||
case Some(version) =>
|
||||
launcher.setDefaultVersion(version)
|
||||
case None =>
|
||||
launcher.printDefaultVersion()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private def upgradeCommand: Command[Config => Unit] =
|
||||
Command("upgrade", "Upgrade the launcher.") {
|
||||
val version = Opts.optionalArgument[String](
|
||||
"VERSION",
|
||||
"VERSION specifies which launcher version to upgrade to. " +
|
||||
"If not provided, defaults to latest version."
|
||||
)
|
||||
version map { version => (_: Config) =>
|
||||
version match {
|
||||
case Some(version) =>
|
||||
println(s"(Not implemented) Upgrade launcher to $version.")
|
||||
case None =>
|
||||
println("(Not implemented) Upgrade launcher to latest version.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private def installEngineCommand: Command[Config => Unit] =
|
||||
Command(
|
||||
"engine",
|
||||
"Install the specified engine VERSION, defaulting to the latest if " +
|
||||
"unspecified."
|
||||
) {
|
||||
val version = Opts.optionalArgument[SemVer]("VERSION")
|
||||
version map { version => (config: Config) =>
|
||||
version match {
|
||||
case Some(value) =>
|
||||
Launcher(config).installEngine(value)
|
||||
case None =>
|
||||
Launcher(config).installLatestEngine()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private def installDistributionCommand: Command[Config => Unit] =
|
||||
Command(
|
||||
"distribution",
|
||||
"Install Enso on the system, deactivating portable mode."
|
||||
) {
|
||||
|
||||
implicit val bundleActionParser: Argument[BundleAction] = {
|
||||
case "move" => DistributionInstaller.MoveBundles.asRight
|
||||
case "copy" => DistributionInstaller.CopyBundles.asRight
|
||||
case "ignore" => DistributionInstaller.IgnoreBundles.asRight
|
||||
case other =>
|
||||
OptsParseError.left(
|
||||
s"`$other` is not a valid bundle-install-mode value. " +
|
||||
s"Possible values are: `move`, `copy`, `ignore`."
|
||||
)
|
||||
}
|
||||
val bundleAction = Opts.optionalParameter[BundleAction](
|
||||
"bundle-install-mode",
|
||||
"(move | copy | ignore)",
|
||||
"Specifies how bundled engines and runtimes should be treated. " +
|
||||
"If `auto-confirm` is set, defaults to move.",
|
||||
showInUsage = false
|
||||
)
|
||||
|
||||
val doNotRemoveOldLauncher = Opts.flag(
|
||||
"no-remove-old-launcher",
|
||||
"If `auto-confirm` is set, the default behavior is to remove the old " +
|
||||
"launcher after installing the distribution. Setting this flag may " +
|
||||
"override this behavior to keep the original launcher.",
|
||||
showInUsage = true
|
||||
)
|
||||
|
||||
(bundleAction, doNotRemoveOldLauncher) mapN {
|
||||
(bundleAction, doNotRemoveOldLauncher) => (config: Config) =>
|
||||
new DistributionInstaller(
|
||||
DistributionManager,
|
||||
config.autoConfirm,
|
||||
removeOldLauncher = !doNotRemoveOldLauncher,
|
||||
bundleActionOption =
|
||||
if (config.autoConfirm)
|
||||
Some(bundleAction.getOrElse(DistributionInstaller.MoveBundles))
|
||||
else bundleAction
|
||||
).install()
|
||||
}
|
||||
}
|
||||
|
||||
private def installCommand: Command[Config => Unit] =
|
||||
Command(
|
||||
"install",
|
||||
"Install a new version of engine or install the distribution locally."
|
||||
) {
|
||||
Opts.subcommands(installEngineCommand, installDistributionCommand)
|
||||
}
|
||||
|
||||
private def uninstallEngineCommand: Command[Config => Unit] =
|
||||
Command(
|
||||
"engine",
|
||||
"Uninstall the provided engine version. If the corresponding runtime " +
|
||||
"is not used by any remaining engine installations, it is also removed."
|
||||
) {
|
||||
val version = Opts.positionalArgument[SemVer]("VERSION")
|
||||
version map { version => (config: Config) =>
|
||||
Launcher(config).uninstallEngine(version)
|
||||
}
|
||||
}
|
||||
|
||||
private def uninstallDistributionCommand: Command[Config => Unit] =
|
||||
Command(
|
||||
"distribution",
|
||||
"Uninstall whole Enso distribution and all components managed by " +
|
||||
"it. If `auto-confirm` is set, it will not attempt to remove the " +
|
||||
"ENSO_DATA_DIRECTORY and ENSO_CONFIG_DIRECTORY if they contain any " +
|
||||
"unexpected files."
|
||||
) {
|
||||
Opts.pure(()) map { (_: Unit) => (config: Config) =>
|
||||
new DistributionUninstaller(
|
||||
DistributionManager,
|
||||
autoConfirm = config.autoConfirm
|
||||
).uninstall()
|
||||
}
|
||||
}
|
||||
|
||||
private def uninstallCommand: Command[Config => Unit] =
|
||||
Command(
|
||||
"uninstall",
|
||||
"Uninstall an Enso component."
|
||||
) {
|
||||
Opts.subcommands(uninstallEngineCommand, uninstallDistributionCommand)
|
||||
}
|
||||
|
||||
private def listCommand: Command[Config => Unit] =
|
||||
Command("list", "List installed components.") {
|
||||
sealed trait Components
|
||||
case object EnsoComponents extends Components
|
||||
case object RuntimeComponents extends Components
|
||||
implicit val argumentComponent: Argument[Components] = {
|
||||
case "engine" => EnsoComponents.asRight
|
||||
case "runtime" => RuntimeComponents.asRight
|
||||
case other =>
|
||||
OptsParseError.left(
|
||||
s"Unknown argument `$other` - expected `engine`, `runtime` " +
|
||||
"or no argument to print a general summary."
|
||||
)
|
||||
}
|
||||
|
||||
val what = Opts.optionalArgument[Components](
|
||||
"COMPONENT",
|
||||
"COMPONENT can be either `engine`, `runtime` or none. " +
|
||||
"If not specified, prints a summary of all installed components."
|
||||
)
|
||||
what map { what => (config: Config) =>
|
||||
what match {
|
||||
case Some(EnsoComponents) => Launcher(config).listEngines()
|
||||
case Some(RuntimeComponents) => Launcher(config).listRuntimes()
|
||||
case None => Launcher(config).listSummary()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private def configCommand: Command[Config => Unit] =
|
||||
Command("config", "Modify global user configuration.") {
|
||||
val key = Opts.positionalArgument[String](
|
||||
"KEY",
|
||||
"Setting KEYs `author.name` and `author.email` can be used to set a" +
|
||||
" default author and maintainer for newly created projects."
|
||||
)
|
||||
val value = Opts.optionalArgument[String](
|
||||
"VALUE",
|
||||
"Setting VALUE to an empty string removes the key from the " +
|
||||
"configuration. When a VALUE is not provided, current configured " +
|
||||
"value is printed."
|
||||
)
|
||||
(key, value) mapN { (key, value) => (config: Config) =>
|
||||
value match {
|
||||
case Some(value) => Launcher(config).updateConfig(key, value)
|
||||
case None => Launcher(config).printConfig(key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private def helpCommand: Command[Config => Unit] =
|
||||
Command("help", "Display summary of available commands.") {
|
||||
Opts.pure(()) map { _ => (_: Config) => printTopLevelHelp() }
|
||||
}
|
||||
|
||||
private def topLevelOpts: Opts[() => TopLevelBehavior[Config]] = {
|
||||
val version =
|
||||
Opts.flag("version", 'V', "Display version.", showInUsage = true)
|
||||
val json = Opts.flag(
|
||||
"json",
|
||||
"Use JSON instead of plain text for version output.",
|
||||
showInUsage = false
|
||||
)
|
||||
val ensurePortable = Opts.flag(
|
||||
"ensure-portable",
|
||||
"Ensures that the launcher is run in portable mode.",
|
||||
showInUsage = false
|
||||
)
|
||||
val autoConfirm = Opts.flag(
|
||||
"auto-confirm",
|
||||
"Proceeds without asking confirmation questions. Please see the " +
|
||||
"options for the specific subcommand you want to run for the defaults " +
|
||||
"used by this option.",
|
||||
showInUsage = false
|
||||
)
|
||||
val hideProgress = Opts.flag(
|
||||
"hide-progress",
|
||||
"Suppresses displaying progress bars for downloads and other long " +
|
||||
"running actions. May be needed if program output is piped.",
|
||||
showInUsage = false
|
||||
)
|
||||
val internalOpts = InternalOpts.topLevelOptions
|
||||
|
||||
(
|
||||
internalOpts,
|
||||
version,
|
||||
json,
|
||||
ensurePortable,
|
||||
autoConfirm,
|
||||
hideProgress
|
||||
) mapN {
|
||||
(_, version, useJSON, shouldEnsurePortable, autoConfirm, hideProgress) =>
|
||||
() =>
|
||||
if (shouldEnsurePortable) {
|
||||
Launcher.ensurePortable()
|
||||
}
|
||||
|
||||
val globalCLIOptions = GlobalCLIOptions(
|
||||
autoConfirm = autoConfirm,
|
||||
hideProgress = hideProgress,
|
||||
useJSON = useJSON
|
||||
)
|
||||
|
||||
if (version) {
|
||||
Launcher(globalCLIOptions).displayVersion(useJSON)
|
||||
TopLevelBehavior.Halt
|
||||
} else
|
||||
TopLevelBehavior.Continue(globalCLIOptions)
|
||||
}
|
||||
}
|
||||
|
||||
private val application: Application[Config] =
|
||||
Application(
|
||||
"enso",
|
||||
"Enso",
|
||||
"Enso Launcher",
|
||||
topLevelOpts,
|
||||
NonEmptyList.of(
|
||||
versionCommand,
|
||||
helpCommand,
|
||||
newCommand,
|
||||
replCommand,
|
||||
runCommand,
|
||||
languageServerCommand,
|
||||
defaultCommand,
|
||||
installCommand,
|
||||
uninstallCommand,
|
||||
upgradeCommand,
|
||||
listCommand,
|
||||
configCommand
|
||||
),
|
||||
PluginManager
|
||||
)
|
||||
|
||||
private def printTopLevelHelp(): Unit = {
|
||||
CLIOutput.println(application.renderHelp())
|
||||
}
|
||||
|
||||
private def setup(): Unit =
|
||||
System.setProperty(
|
||||
"org.apache.commons.logging.Log",
|
||||
"org.apache.commons.logging.impl.NoOpLog"
|
||||
)
|
||||
|
||||
private def runAppHandlingParseErrors(args: Array[String]): Int =
|
||||
LauncherApplication.application.run(args) match {
|
||||
case Left(errors) =>
|
||||
CLIOutput.println(errors.mkString("\n"))
|
||||
1
|
||||
case Right(()) =>
|
||||
0
|
||||
}
|
||||
|
||||
def main(args: Array[String]): Unit = {
|
||||
setup()
|
||||
val exitCode =
|
||||
try {
|
||||
application.run(args) match {
|
||||
case Left(errors) =>
|
||||
CLIOutput.println(errors.mkString("\n"))
|
||||
1
|
||||
case Right(()) =>
|
||||
0
|
||||
LauncherUpgrader.recoverUpgradeRequiredErrors(args) {
|
||||
runAppHandlingParseErrors(args)
|
||||
}
|
||||
} catch {
|
||||
case e: Exception =>
|
||||
|
@ -1,7 +1,8 @@
|
||||
package org.enso.launcher.components
|
||||
|
||||
import nl.gn0s1s.bump.SemVer
|
||||
import org.enso.launcher.Launcher
|
||||
import org.enso.launcher.CurrentVersion
|
||||
import org.enso.launcher.cli.GlobalCLIOptions
|
||||
|
||||
/**
|
||||
* A base class for exceptions caused by [[ComponentsManager]] logic.
|
||||
@ -32,20 +33,31 @@ case class InstallationError(message: String, cause: Throwable = null)
|
||||
|
||||
/**
|
||||
* Indicates that a requested engine version requires a newer launcher version.
|
||||
*
|
||||
* This error can be recovered by
|
||||
* [[org.enso.launcher.upgrade.LauncherUpgrader.recoverUpgradeRequiredErrors]]
|
||||
* which can perform the upgrade and re-run the requested command with the
|
||||
* newer version.
|
||||
*
|
||||
* @param expectedLauncherVersion the minimum launcher version that is required
|
||||
* @param globalCLIOptions the CLI options that should be passed to an upgrader
|
||||
* if an upgrade is requested
|
||||
*/
|
||||
case class LauncherUpgradeRequiredError(expectedVersion: SemVer)
|
||||
extends ComponentsException(
|
||||
case class LauncherUpgradeRequiredError(
|
||||
expectedLauncherVersion: SemVer,
|
||||
globalCLIOptions: GlobalCLIOptions
|
||||
) extends ComponentsException(
|
||||
s"Minimum launcher version required to use this engine is " +
|
||||
s"$expectedVersion"
|
||||
s"$expectedLauncherVersion"
|
||||
) {
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
override def toString: String =
|
||||
s"This launcher version is ${Launcher.version}, but $expectedVersion " +
|
||||
s"is required to run this engine. If you want to use it, upgrade the " +
|
||||
s"launcher with `enso upgrade`."
|
||||
s"This launcher version is ${CurrentVersion.version}, but " +
|
||||
s"$expectedLauncherVersion is required to run this engine. If you want " +
|
||||
s"to use it, upgrade the launcher with `enso upgrade`."
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -8,12 +8,13 @@ import org.enso.launcher.FileSystem.PathSyntax
|
||||
import org.enso.launcher.archive.Archive
|
||||
import org.enso.launcher.cli.GlobalCLIOptions
|
||||
import org.enso.launcher.installation.DistributionManager
|
||||
import org.enso.launcher.releases.{
|
||||
EngineReleaseProvider,
|
||||
import org.enso.launcher.releases.engine.EngineRelease
|
||||
import org.enso.launcher.releases.runtime.{
|
||||
GraalCEReleaseProvider,
|
||||
RuntimeReleaseProvider
|
||||
}
|
||||
import org.enso.launcher.{FileSystem, Launcher, Logger}
|
||||
import org.enso.launcher.releases.{EnsoRepository, ReleaseProvider}
|
||||
import org.enso.launcher.{FileSystem, Logger}
|
||||
|
||||
import scala.util.control.NonFatal
|
||||
import scala.util.{Failure, Success, Try, Using}
|
||||
@ -32,7 +33,7 @@ import scala.util.{Failure, Success, Try, Using}
|
||||
class ComponentsManager(
|
||||
cliOptions: GlobalCLIOptions,
|
||||
distributionManager: DistributionManager,
|
||||
engineReleaseProvider: EngineReleaseProvider,
|
||||
engineReleaseProvider: ReleaseProvider[EngineRelease],
|
||||
runtimeReleaseProvider: RuntimeReleaseProvider
|
||||
) {
|
||||
private val showProgress = !cliOptions.hideProgress
|
||||
@ -238,7 +239,7 @@ class ComponentsManager(
|
||||
* [[engineReleaseProvider]].
|
||||
*/
|
||||
def fetchLatestEngineVersion(): SemVer =
|
||||
engineReleaseProvider.findLatest().get
|
||||
engineReleaseProvider.findLatestVersion().get
|
||||
|
||||
/**
|
||||
* Uninstalls the engine with the provided `version` (if it was installed).
|
||||
@ -266,7 +267,13 @@ class ComponentsManager(
|
||||
* the actual directory after doing simple sanity checks.
|
||||
*/
|
||||
private def installEngine(version: SemVer): Engine = {
|
||||
val engineRelease = engineReleaseProvider.getRelease(version).get
|
||||
val engineRelease = engineReleaseProvider.fetchRelease(version).get
|
||||
if (!engineRelease.manifest.isUsableWithCurrentVersion) {
|
||||
throw LauncherUpgradeRequiredError(
|
||||
engineRelease.manifest.minimumLauncherVersion,
|
||||
cliOptions
|
||||
)
|
||||
}
|
||||
if (engineRelease.isBroken) {
|
||||
if (cliOptions.autoConfirm) {
|
||||
Logger.warn(
|
||||
@ -296,8 +303,8 @@ class ComponentsManager(
|
||||
Logger.debug(s"Downloading packages to $directory")
|
||||
val enginePackage = directory / engineRelease.packageFileName
|
||||
Logger.info(s"Downloading ${enginePackage.getFileName}.")
|
||||
engineReleaseProvider
|
||||
.downloadPackage(engineRelease, enginePackage)
|
||||
engineRelease
|
||||
.downloadPackage(enginePackage)
|
||||
.waitForResult(showProgress)
|
||||
.get
|
||||
|
||||
@ -492,8 +499,13 @@ class ComponentsManager(
|
||||
*/
|
||||
private def loadAndCheckEngineManifest(path: Path): Try[Manifest] = {
|
||||
Manifest.load(path / Manifest.DEFAULT_MANIFEST_NAME).flatMap { manifest =>
|
||||
if (manifest.minimumLauncherVersion > Launcher.version) {
|
||||
Failure(LauncherUpgradeRequiredError(manifest.minimumLauncherVersion))
|
||||
if (!manifest.isUsableWithCurrentVersion) {
|
||||
Failure(
|
||||
LauncherUpgradeRequiredError(
|
||||
manifest.minimumLauncherVersion,
|
||||
cliOptions
|
||||
)
|
||||
)
|
||||
} else Success(manifest)
|
||||
}
|
||||
}
|
||||
@ -609,17 +621,20 @@ class ComponentsManager(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Default [[ComponentsManager]] using the default [[DistributionManager]] and
|
||||
* release providers.
|
||||
*
|
||||
* @param cliOptions options from the CLI setting verbosity of the executed
|
||||
* actions
|
||||
*/
|
||||
case class DefaultComponentsManager(cliOptions: GlobalCLIOptions)
|
||||
extends ComponentsManager(
|
||||
cliOptions,
|
||||
object ComponentsManager {
|
||||
|
||||
/**
|
||||
* Creates a [[ComponentsManager]] using the default [[DistributionManager]]
|
||||
* and release providers.
|
||||
*
|
||||
* @param globalCLIOptions options from the CLI setting verbosity of the
|
||||
* executed actions
|
||||
*/
|
||||
def makeDefault(globalCLIOptions: GlobalCLIOptions): ComponentsManager =
|
||||
new ComponentsManager(
|
||||
globalCLIOptions,
|
||||
DistributionManager,
|
||||
EngineReleaseProvider,
|
||||
EnsoRepository.defaultEngineReleaseProvider,
|
||||
GraalCEReleaseProvider
|
||||
)
|
||||
}
|
||||
|
@ -6,7 +6,7 @@ import java.nio.file.Path
|
||||
import cats.Show
|
||||
import io.circe.{yaml, Decoder}
|
||||
import nl.gn0s1s.bump.SemVer
|
||||
import org.enso.launcher.OS
|
||||
import org.enso.launcher.{CurrentVersion, OS}
|
||||
import org.enso.launcher.components.Manifest.JVMOption
|
||||
import org.enso.pkg.SemVerJson._
|
||||
|
||||
@ -41,6 +41,9 @@ case class Manifest(
|
||||
*/
|
||||
def runtimeVersion: RuntimeVersion =
|
||||
RuntimeVersion(graalVMVersion, graalJavaVersion)
|
||||
|
||||
def isUsableWithCurrentVersion: Boolean =
|
||||
CurrentVersion.version >= minimumLauncherVersion
|
||||
}
|
||||
|
||||
object Manifest {
|
||||
|
@ -0,0 +1,35 @@
|
||||
package org.enso.launcher.releases
|
||||
|
||||
import java.nio.file.Path
|
||||
|
||||
import org.enso.cli.TaskProgress
|
||||
|
||||
/**
|
||||
* Represents a downloadable release asset.
|
||||
*/
|
||||
trait Asset {
|
||||
|
||||
/**
|
||||
* Asset's filename.
|
||||
*/
|
||||
def fileName: String
|
||||
|
||||
/**
|
||||
* Downloads the asset to the provided path.
|
||||
*
|
||||
* The path should include a filename for the asset (not just a parent
|
||||
* directory).
|
||||
*
|
||||
* Returns a [[TaskProgress]] instance that is completed when the download
|
||||
* finishes.
|
||||
*/
|
||||
def downloadTo(path: Path): TaskProgress[Unit]
|
||||
|
||||
/**
|
||||
* Fetches the asset treating it as text data.
|
||||
*
|
||||
* Returns a [[TaskProgress]] instance that will contain a [[String]]
|
||||
* containing the fetched text.
|
||||
*/
|
||||
def fetchAsText(): TaskProgress[String]
|
||||
}
|
@ -1,156 +0,0 @@
|
||||
package org.enso.launcher.releases
|
||||
|
||||
object Placeholder2
|
||||
|
||||
import java.nio.file.Path
|
||||
|
||||
import nl.gn0s1s.bump.SemVer
|
||||
import org.enso.cli.TaskProgress
|
||||
import org.enso.launcher.OS
|
||||
import org.enso.launcher.releases.github.GithubReleaseProvider
|
||||
import org.enso.launcher.components.Manifest
|
||||
|
||||
import scala.util.{Failure, Success, Try}
|
||||
|
||||
/**
|
||||
* Represents an engine release.
|
||||
*
|
||||
* @param version engine version
|
||||
* @param manifest manifest associated with the release
|
||||
* @param isBroken specifies whether this release is marked as broken
|
||||
* @param release a [[Release]] that allows to download assets
|
||||
*/
|
||||
case class EngineRelease(
|
||||
version: SemVer,
|
||||
manifest: Manifest,
|
||||
isBroken: Boolean,
|
||||
release: Release
|
||||
) {
|
||||
|
||||
/**
|
||||
* Determines the filename of the package that should be downloaded from this
|
||||
* release.
|
||||
*
|
||||
* That filename may be platform specific.
|
||||
*/
|
||||
def packageFileName: String = {
|
||||
val os = OS.operatingSystem match {
|
||||
case OS.Linux => "linux"
|
||||
case OS.MacOS => "macos"
|
||||
case OS.Windows => "windows"
|
||||
}
|
||||
val arch = OS.architecture
|
||||
val extension = OS.operatingSystem match {
|
||||
case OS.Linux => ".tar.gz"
|
||||
case OS.MacOS => ".tar.gz"
|
||||
case OS.Windows => ".zip"
|
||||
}
|
||||
s"enso-engine-$version-$os-$arch$extension"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps a generic [[ReleaseProvider]] to provide engine releases from it.
|
||||
*/
|
||||
class EngineReleaseProvider(releaseProvider: ReleaseProvider) {
|
||||
private val tagPrefix = "enso-"
|
||||
|
||||
/**
|
||||
* Returns the version of the most recent engine release.
|
||||
*
|
||||
* It ignores releases marked as broken, so the latest non-broken release is
|
||||
* returned.
|
||||
*/
|
||||
def findLatest(): Try[SemVer] =
|
||||
releaseProvider.listReleases().flatMap { releases =>
|
||||
val versions =
|
||||
releases
|
||||
.filter(!isBroken(_))
|
||||
.map(_.tag.stripPrefix(tagPrefix))
|
||||
.flatMap(SemVer(_))
|
||||
versions.sorted.lastOption.map(Success(_)).getOrElse {
|
||||
Failure(ReleaseProviderException("No valid engine versions were found"))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches release metadata for a given version.
|
||||
*/
|
||||
def getRelease(version: SemVer): Try[EngineRelease] = {
|
||||
val tag = tagPrefix + version.toString
|
||||
for {
|
||||
release <- releaseProvider.releaseForTag(tag)
|
||||
manifestAsset <-
|
||||
release.assets
|
||||
.find(_.fileName == Manifest.DEFAULT_MANIFEST_NAME)
|
||||
.toRight(
|
||||
ReleaseProviderException(
|
||||
s"${Manifest.DEFAULT_MANIFEST_NAME} file is mising from " +
|
||||
s"release assets."
|
||||
)
|
||||
)
|
||||
.toTry
|
||||
manifestContent <- manifestAsset.fetchAsText().waitForResult()
|
||||
manifest <-
|
||||
Manifest
|
||||
.fromYaml(manifestContent)
|
||||
.recoverWith(error =>
|
||||
Failure(
|
||||
ReleaseProviderException(
|
||||
"Cannot parse attached manifest file.",
|
||||
error
|
||||
)
|
||||
)
|
||||
)
|
||||
} yield EngineRelease(
|
||||
version = version,
|
||||
isBroken = isBroken(release),
|
||||
manifest = manifest,
|
||||
release = release
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the given release is broken.
|
||||
*/
|
||||
private def isBroken(release: Release): Boolean =
|
||||
release.assets.exists(_.fileName == "broken")
|
||||
|
||||
/**
|
||||
* Downloads the package associated with the given release into
|
||||
* `destination`.
|
||||
*
|
||||
* @param release the release to download the package from
|
||||
* @param destination name of the file that will be created to contain the
|
||||
* downloaded package
|
||||
*/
|
||||
def downloadPackage(
|
||||
release: EngineRelease,
|
||||
destination: Path
|
||||
): TaskProgress[Unit] = {
|
||||
val packageName = release.packageFileName
|
||||
release.release.assets
|
||||
.find(_.fileName == packageName)
|
||||
.map(_.downloadTo(destination))
|
||||
.getOrElse {
|
||||
TaskProgress.immediateFailure(
|
||||
ReleaseProviderException(
|
||||
s"Cannot find package `$packageName` in the release."
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Default [[EngineReleaseProvider]] that uses the GitHub Release API.
|
||||
*/
|
||||
object EngineReleaseProvider
|
||||
extends EngineReleaseProvider(
|
||||
new GithubReleaseProvider(
|
||||
"enso-org",
|
||||
"enso-staging" // TODO [RW] The release provider will be moved from
|
||||
// staging to the main repository, when the first official Enso release
|
||||
// is released.
|
||||
)
|
||||
)
|
@ -0,0 +1,68 @@
|
||||
package org.enso.launcher.releases
|
||||
import nl.gn0s1s.bump.SemVer
|
||||
import org.enso.launcher.OS
|
||||
|
||||
import scala.util.{Failure, Success, Try}
|
||||
|
||||
/**
|
||||
* A helper class that implements the shared logic of engine and launcher
|
||||
* releases which handles listing releases excluding broken ones.
|
||||
*
|
||||
* @param simpleReleaseProvider a base release provided that provides the raw
|
||||
* releases
|
||||
* @tparam ReleaseType type of the specific component's release, containing any
|
||||
* necessary metadata
|
||||
*/
|
||||
abstract class EnsoReleaseProvider[ReleaseType](
|
||||
simpleReleaseProvider: SimpleReleaseProvider
|
||||
) extends ReleaseProvider[ReleaseType] {
|
||||
protected val tagPrefix = "enso-"
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
override def findLatestVersion(): Try[SemVer] =
|
||||
fetchAllValidVersions().flatMap { versions =>
|
||||
versions.sorted.lastOption.map(Success(_)).getOrElse {
|
||||
Failure(ReleaseProviderException("No valid engine versions were found"))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
override def fetchAllValidVersions(): Try[Seq[SemVer]] =
|
||||
simpleReleaseProvider.listReleases().map { releases =>
|
||||
releases
|
||||
.filter(!_.isMarkedBroken)
|
||||
.map(_.tag.stripPrefix(tagPrefix))
|
||||
.flatMap(SemVer(_))
|
||||
}
|
||||
}
|
||||
|
||||
object EnsoReleaseProvider {
|
||||
|
||||
/**
|
||||
* Returns a full system-dependent package name for a component with a given
|
||||
* name and version.
|
||||
*
|
||||
* The package name can depend on the current OS and its architecture.
|
||||
*/
|
||||
def packageNameForComponent(
|
||||
componentName: String,
|
||||
version: SemVer
|
||||
): String = {
|
||||
val os = OS.operatingSystem match {
|
||||
case OS.Linux => "linux"
|
||||
case OS.MacOS => "macos"
|
||||
case OS.Windows => "windows"
|
||||
}
|
||||
val arch = OS.architecture
|
||||
val extension = OS.operatingSystem match {
|
||||
case OS.Linux => ".tar.gz"
|
||||
case OS.MacOS => ".tar.gz"
|
||||
case OS.Windows => ".zip"
|
||||
}
|
||||
s"enso-$componentName-$version-$os-$arch$extension"
|
||||
}
|
||||
}
|
@ -0,0 +1,68 @@
|
||||
package org.enso.launcher.releases
|
||||
|
||||
import java.nio.file.Path
|
||||
|
||||
import org.enso.launcher.Logger
|
||||
import org.enso.launcher.releases.engine.{EngineRelease, EngineReleaseProvider}
|
||||
import org.enso.launcher.releases.github.GithubReleaseProvider
|
||||
import org.enso.launcher.releases.launcher.{
|
||||
LauncherRelease,
|
||||
LauncherReleaseProvider
|
||||
}
|
||||
import org.enso.launcher.releases.testing.FakeReleaseProvider
|
||||
|
||||
/**
|
||||
* Represents the default Enso repository providing releases for the engine and
|
||||
* the launcher.
|
||||
*
|
||||
* In test mode, the default GitHub repository can be overridden with a local
|
||||
* filesystem-backed repository.
|
||||
*/
|
||||
object EnsoRepository {
|
||||
private var currentRepository: SimpleReleaseProvider =
|
||||
// TODO [RW] The release provider will be moved from staging to the main
|
||||
// repository, when the first official Enso release is released.
|
||||
new GithubReleaseProvider(
|
||||
"enso-org",
|
||||
"enso-staging"
|
||||
)
|
||||
|
||||
/**
|
||||
* Default repository for Enso releases.
|
||||
*/
|
||||
def defaultReleaseRepository: SimpleReleaseProvider = currentRepository
|
||||
|
||||
/**
|
||||
* Default provider of engine releases.
|
||||
*/
|
||||
def defaultEngineReleaseProvider: ReleaseProvider[EngineRelease] =
|
||||
new EngineReleaseProvider(defaultReleaseRepository)
|
||||
|
||||
/**
|
||||
* Default provider of launcher releases.
|
||||
*/
|
||||
def defaultLauncherReleaseProvider: ReleaseProvider[LauncherRelease] =
|
||||
new LauncherReleaseProvider(defaultReleaseRepository)
|
||||
|
||||
/**
|
||||
* Overrides the default repository with a local filesystem based fake
|
||||
* repository.
|
||||
*
|
||||
* Internal method used for testing.
|
||||
*/
|
||||
def internalUseFakeRepository(fakeRepositoryRoot: Path): Unit =
|
||||
if (buildinfo.Info.isRelease)
|
||||
throw new IllegalStateException(
|
||||
"Internal testing function internalUseFakeRepository used in a " +
|
||||
"release build."
|
||||
)
|
||||
else {
|
||||
Logger.debug(s"[TEST] Using a fake repository at $fakeRepositoryRoot.")
|
||||
currentRepository = makeFakeRepository(fakeRepositoryRoot)
|
||||
}
|
||||
|
||||
private def makeFakeRepository(
|
||||
fakeRepositoryRoot: Path
|
||||
): SimpleReleaseProvider =
|
||||
FakeReleaseProvider(fakeRepositoryRoot)
|
||||
}
|
@ -1,22 +1,8 @@
|
||||
package org.enso.launcher.releases
|
||||
|
||||
import java.nio.file.Path
|
||||
|
||||
import org.enso.cli.TaskProgress
|
||||
|
||||
import scala.util.Try
|
||||
|
||||
/**
|
||||
* Represents a downloadable release asset.
|
||||
*/
|
||||
trait Asset {
|
||||
def fileName: String
|
||||
def downloadTo(path: Path): TaskProgress[Unit]
|
||||
def fetchAsText(): TaskProgress[String]
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps a generic release returned by [[ReleaseProvider]].
|
||||
* Wraps a generic release identified by a tag and containing a sequence of
|
||||
* assets.
|
||||
*/
|
||||
trait Release {
|
||||
|
||||
@ -29,30 +15,9 @@ trait Release {
|
||||
* The sequence of assets available in this release.
|
||||
*/
|
||||
def assets: Seq[Asset]
|
||||
}
|
||||
|
||||
/**
|
||||
* A generic release provider that allows to list and download releases.
|
||||
*/
|
||||
trait ReleaseProvider {
|
||||
|
||||
/**
|
||||
* Finds a release for the given tag.
|
||||
* Checks if the given release is marked as broken.
|
||||
*/
|
||||
def releaseForTag(tag: String): Try[Release]
|
||||
|
||||
/**
|
||||
* Fetches a list of all releases.
|
||||
*/
|
||||
def listReleases(): Try[Seq[Release]]
|
||||
}
|
||||
|
||||
case class ReleaseProviderException(message: String, cause: Throwable = null)
|
||||
extends RuntimeException(message, cause) {
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
override def toString: String =
|
||||
s"A problem occurred when trying to find the release: $message"
|
||||
def isMarkedBroken: Boolean = assets.exists(_.fileName == "broken")
|
||||
}
|
||||
|
@ -0,0 +1,33 @@
|
||||
package org.enso.launcher.releases
|
||||
|
||||
import nl.gn0s1s.bump.SemVer
|
||||
|
||||
import scala.util.Try
|
||||
|
||||
/**
|
||||
* A high-level release provider that includes release metadata.
|
||||
* @tparam ReleaseType type of a specific component's release, containing any
|
||||
* necessary metadata
|
||||
*/
|
||||
trait ReleaseProvider[ReleaseType] {
|
||||
|
||||
/**
|
||||
* Returns the version of the most recent release.
|
||||
*
|
||||
* It ignores releases marked as broken, so the latest non-broken release is
|
||||
* returned.
|
||||
*/
|
||||
def findLatestVersion(): Try[SemVer]
|
||||
|
||||
/**
|
||||
* Returns sequence of available non-broken versions.
|
||||
*
|
||||
* The sequence does not have to be sorted.
|
||||
*/
|
||||
def fetchAllValidVersions(): Try[Seq[SemVer]]
|
||||
|
||||
/**
|
||||
* Fetch release metadata for the given version.
|
||||
*/
|
||||
def fetchRelease(version: SemVer): Try[ReleaseType]
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
package org.enso.launcher.releases
|
||||
|
||||
/**
|
||||
* Indicates a release provider failure.
|
||||
*/
|
||||
case class ReleaseProviderException(message: String, cause: Throwable = null)
|
||||
extends RuntimeException(message, cause) {
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
override def toString: String =
|
||||
s"A problem occurred when trying to find the release: $message"
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
package org.enso.launcher.releases
|
||||
|
||||
import scala.util.Try
|
||||
|
||||
/**
|
||||
* A generic release provider that allows to list and download releases.
|
||||
*/
|
||||
trait SimpleReleaseProvider {
|
||||
|
||||
/**
|
||||
* Finds a release for the given tag.
|
||||
*/
|
||||
def releaseForTag(tag: String): Try[Release]
|
||||
|
||||
/**
|
||||
* Fetches a list of all releases.
|
||||
*/
|
||||
def listReleases(): Try[Seq[Release]]
|
||||
}
|
@ -0,0 +1,49 @@
|
||||
package org.enso.launcher.releases.engine
|
||||
|
||||
import java.nio.file.Path
|
||||
|
||||
import nl.gn0s1s.bump.SemVer
|
||||
import org.enso.cli.TaskProgress
|
||||
import org.enso.launcher.components.Manifest
|
||||
|
||||
/**
|
||||
* Represents an engine release.
|
||||
*/
|
||||
trait EngineRelease {
|
||||
|
||||
/**
|
||||
* Engine version.
|
||||
*/
|
||||
def version: SemVer
|
||||
|
||||
/**
|
||||
* Manifest associated with the release.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
def manifest: Manifest
|
||||
|
||||
/**
|
||||
* Specifies whether this release is marked as broken.
|
||||
* @return
|
||||
*/
|
||||
def isBroken: Boolean
|
||||
|
||||
/**
|
||||
* Determines the filename of the package that should be downloaded from this
|
||||
* release.
|
||||
*
|
||||
* That filename may be platform specific.
|
||||
*/
|
||||
def packageFileName: String
|
||||
|
||||
/**
|
||||
* Downloads the package associated with the release into `destination`.
|
||||
*
|
||||
* @param destination name of the file that will be created to contain the
|
||||
* downloaded package
|
||||
*/
|
||||
def downloadPackage(
|
||||
destination: Path
|
||||
): TaskProgress[Unit]
|
||||
}
|
@ -0,0 +1,85 @@
|
||||
package org.enso.launcher.releases.engine
|
||||
|
||||
import java.nio.file.Path
|
||||
|
||||
import nl.gn0s1s.bump.SemVer
|
||||
import org.enso.cli.TaskProgress
|
||||
import org.enso.launcher.components.Manifest
|
||||
import org.enso.launcher.releases.{
|
||||
EnsoReleaseProvider,
|
||||
Release,
|
||||
ReleaseProviderException,
|
||||
SimpleReleaseProvider
|
||||
}
|
||||
|
||||
import scala.util.{Failure, Try}
|
||||
|
||||
/**
|
||||
* Wraps a generic [[SimpleReleaseProvider]] to provide engine releases.
|
||||
*/
|
||||
class EngineReleaseProvider(releaseProvider: SimpleReleaseProvider)
|
||||
extends EnsoReleaseProvider[EngineRelease](releaseProvider) {
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
def fetchRelease(version: SemVer): Try[EngineRelease] = {
|
||||
val tag = tagPrefix + version.toString
|
||||
for {
|
||||
release <- releaseProvider.releaseForTag(tag)
|
||||
manifestAsset <-
|
||||
release.assets
|
||||
.find(_.fileName == Manifest.DEFAULT_MANIFEST_NAME)
|
||||
.toRight(
|
||||
ReleaseProviderException(
|
||||
s"${Manifest.DEFAULT_MANIFEST_NAME} file is missing from " +
|
||||
s"release assets."
|
||||
)
|
||||
)
|
||||
.toTry
|
||||
manifestContent <- manifestAsset.fetchAsText().waitForResult()
|
||||
manifest <-
|
||||
Manifest
|
||||
.fromYaml(manifestContent)
|
||||
.recoverWith(error =>
|
||||
Failure(
|
||||
ReleaseProviderException(
|
||||
"Cannot parse attached manifest file.",
|
||||
error
|
||||
)
|
||||
)
|
||||
)
|
||||
} yield DefaultEngineRelease(
|
||||
version = version,
|
||||
isBroken = release.isMarkedBroken,
|
||||
manifest = manifest,
|
||||
release = release
|
||||
)
|
||||
}
|
||||
|
||||
private case class DefaultEngineRelease(
|
||||
version: SemVer,
|
||||
manifest: Manifest,
|
||||
isBroken: Boolean,
|
||||
release: Release
|
||||
) extends EngineRelease {
|
||||
def packageFileName: String =
|
||||
EnsoReleaseProvider.packageNameForComponent("engine", version)
|
||||
|
||||
def downloadPackage(
|
||||
destination: Path
|
||||
): TaskProgress[Unit] = {
|
||||
val packageName = packageFileName
|
||||
release.assets
|
||||
.find(_.fileName == packageName)
|
||||
.map(_.downloadTo(destination))
|
||||
.getOrElse {
|
||||
TaskProgress.immediateFailure(
|
||||
ReleaseProviderException(
|
||||
s"Cannot find package `$packageName` in the release."
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,11 +1,11 @@
|
||||
package org.enso.launcher.releases.github
|
||||
|
||||
import org.enso.launcher.releases.{Release, ReleaseProvider}
|
||||
import org.enso.launcher.releases.{Release, SimpleReleaseProvider}
|
||||
|
||||
import scala.util.Try
|
||||
|
||||
/**
|
||||
* Implements [[ReleaseProvider]] providing releases from a specified GitHub
|
||||
* Implements [[SimpleReleaseProvider]] providing releases from a specified GitHub
|
||||
* repository using the GitHub Release API.
|
||||
*
|
||||
* @param owner owner of the repository
|
||||
@ -14,7 +14,7 @@ import scala.util.Try
|
||||
class GithubReleaseProvider(
|
||||
owner: String,
|
||||
repositoryName: String
|
||||
) extends ReleaseProvider {
|
||||
) extends SimpleReleaseProvider {
|
||||
private val repo = GithubAPI.Repository(owner, repositoryName)
|
||||
|
||||
/**
|
||||
|
@ -0,0 +1,76 @@
|
||||
package org.enso.launcher.releases.launcher
|
||||
|
||||
import io.circe.{yaml, Decoder}
|
||||
import nl.gn0s1s.bump.SemVer
|
||||
import org.enso.launcher.releases.ReleaseProviderException
|
||||
import org.enso.pkg.SemVerJson._
|
||||
|
||||
import scala.util.{Failure, Try}
|
||||
|
||||
/**
|
||||
* Contains release metadata associated with a launcher release.
|
||||
*
|
||||
* @param minimumVersionForUpgrade minimum version of the current launcher that
|
||||
* is required to upgrade to this version; if
|
||||
* current launcher is older than that provided
|
||||
* version, a multi-step upgrade must be
|
||||
* performed
|
||||
* @param filesToCopy a sequence of filenames of files that should be updated
|
||||
* in the data root
|
||||
* @param directoriesToCopy a sequence of names of directories that should be
|
||||
* updated in the data root
|
||||
*/
|
||||
case class LauncherManifest(
|
||||
minimumVersionForUpgrade: SemVer,
|
||||
filesToCopy: Seq[String],
|
||||
directoriesToCopy: Seq[String]
|
||||
)
|
||||
|
||||
object LauncherManifest {
|
||||
|
||||
/**
|
||||
* Default name of the asset containing the launcher manifest.
|
||||
*/
|
||||
val assetName: String = "launcher-manifest.yaml"
|
||||
|
||||
private object Fields {
|
||||
val minimumVersionForUpgrade = "minimum-version-for-upgrade"
|
||||
val filesToCopy = "files-to-copy"
|
||||
val directoriesToCopy = "directories-to-copy"
|
||||
}
|
||||
|
||||
/**
|
||||
* [[Decoder]] instance for [[LauncherManifest]].
|
||||
*/
|
||||
implicit val decoder: Decoder[LauncherManifest] = { json =>
|
||||
for {
|
||||
minimumVersionToUpgrade <-
|
||||
json.get[SemVer](Fields.minimumVersionForUpgrade)
|
||||
files <- json.getOrElse[Seq[String]](Fields.filesToCopy)(Seq())
|
||||
directories <-
|
||||
json.getOrElse[Seq[String]](Fields.directoriesToCopy)(Seq())
|
||||
} yield LauncherManifest(
|
||||
minimumVersionForUpgrade = minimumVersionToUpgrade,
|
||||
filesToCopy = files,
|
||||
directoriesToCopy = directories
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to parse the [[LauncherManifest]] from a [[String]].
|
||||
*/
|
||||
def fromYAML(string: String): Try[LauncherManifest] =
|
||||
yaml.parser
|
||||
.parse(string)
|
||||
.flatMap(_.as[LauncherManifest])
|
||||
.toTry
|
||||
.recoverWith { error =>
|
||||
// TODO [RW] more readable errors in #1111
|
||||
Failure(
|
||||
ReleaseProviderException(
|
||||
s"Cannot parse launcher manifest: $error.",
|
||||
error
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
@ -0,0 +1,56 @@
|
||||
package org.enso.launcher.releases.launcher
|
||||
|
||||
import java.nio.file.Path
|
||||
|
||||
import nl.gn0s1s.bump.SemVer
|
||||
import org.enso.cli.TaskProgress
|
||||
import org.enso.launcher.CurrentVersion
|
||||
|
||||
/**
|
||||
* Represents a launcher release.
|
||||
*/
|
||||
trait LauncherRelease {
|
||||
|
||||
/**
|
||||
* Version of the release.
|
||||
*/
|
||||
def version: SemVer
|
||||
|
||||
/**
|
||||
* Minimum version of the launcher that is required to upgrade to this
|
||||
* release.
|
||||
*/
|
||||
def minimumVersionToPerformUpgrade: SemVer =
|
||||
manifest.minimumVersionForUpgrade
|
||||
|
||||
/**
|
||||
* Manifest associated with the release.
|
||||
*/
|
||||
def manifest: LauncherManifest
|
||||
|
||||
/**
|
||||
* Specifies if the release is marked as broken and should not generally be
|
||||
* used unless explicitly asked to.
|
||||
*/
|
||||
def isMarkedBroken: Boolean
|
||||
|
||||
/**
|
||||
* Name of the asset containing the launcher package for the current platform
|
||||
* inside of this release.
|
||||
*/
|
||||
def packageFileName: String
|
||||
|
||||
/**
|
||||
* Downloads the launcher package to the specified destination.
|
||||
*/
|
||||
def downloadPackage(path: Path): TaskProgress[Unit]
|
||||
|
||||
/**
|
||||
* Checks if the current launcher version is allowed to upgrade directly to
|
||||
* this release.
|
||||
*
|
||||
* If false, a multi-step upgrade must be performed.
|
||||
*/
|
||||
def canPerformUpgradeFromCurrentVersion: Boolean =
|
||||
CurrentVersion.version >= minimumVersionToPerformUpgrade
|
||||
}
|
@ -0,0 +1,67 @@
|
||||
package org.enso.launcher.releases.launcher
|
||||
|
||||
import java.nio.file.Path
|
||||
|
||||
import nl.gn0s1s.bump.SemVer
|
||||
import org.enso.cli.TaskProgress
|
||||
import org.enso.launcher.releases.{
|
||||
EnsoReleaseProvider,
|
||||
Release,
|
||||
ReleaseProviderException,
|
||||
SimpleReleaseProvider
|
||||
}
|
||||
|
||||
import scala.util.Try
|
||||
|
||||
/**
|
||||
* Wraps a generic [[SimpleReleaseProvider]] to provide launcher releases.
|
||||
*/
|
||||
class LauncherReleaseProvider(releaseProvider: SimpleReleaseProvider)
|
||||
extends EnsoReleaseProvider[LauncherRelease](releaseProvider) {
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
override def fetchRelease(version: SemVer): Try[LauncherRelease] = {
|
||||
val tag = tagPrefix + version.toString
|
||||
for {
|
||||
release <- releaseProvider.releaseForTag(tag)
|
||||
manifestAsset <-
|
||||
release.assets
|
||||
.find(_.fileName == LauncherManifest.assetName)
|
||||
.toRight(
|
||||
ReleaseProviderException(
|
||||
s"${LauncherManifest.assetName} file is missing from release " +
|
||||
s"assets."
|
||||
)
|
||||
)
|
||||
.toTry
|
||||
manifestContent <- manifestAsset.fetchAsText().waitForResult()
|
||||
manifest <- LauncherManifest.fromYAML(manifestContent)
|
||||
} yield GitHubLauncherRelease(version, manifest, release)
|
||||
}
|
||||
|
||||
private case class GitHubLauncherRelease(
|
||||
version: SemVer,
|
||||
manifest: LauncherManifest,
|
||||
release: Release
|
||||
) extends LauncherRelease {
|
||||
override def packageFileName: String =
|
||||
EnsoReleaseProvider.packageNameForComponent("launcher", version)
|
||||
override def downloadPackage(path: Path): TaskProgress[Unit] = {
|
||||
val packageName = packageFileName
|
||||
release.assets
|
||||
.find(_.fileName == packageName)
|
||||
.map(_.downloadTo(path))
|
||||
.getOrElse {
|
||||
TaskProgress.immediateFailure(
|
||||
ReleaseProviderException(
|
||||
s"Cannot find package `$packageName` in the release."
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override def isMarkedBroken: Boolean = release.isMarkedBroken
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package org.enso.launcher.releases
|
||||
package org.enso.launcher.releases.runtime
|
||||
|
||||
import java.nio.file.Path
|
||||
|
||||
@ -6,14 +6,18 @@ import org.enso.cli.TaskProgress
|
||||
import org.enso.launcher.OS
|
||||
import org.enso.launcher.components.RuntimeVersion
|
||||
import org.enso.launcher.releases.github.GithubReleaseProvider
|
||||
import org.enso.launcher.releases.{
|
||||
ReleaseProviderException,
|
||||
SimpleReleaseProvider
|
||||
}
|
||||
|
||||
import scala.util.{Failure, Success}
|
||||
|
||||
/**
|
||||
* [[RuntimeReleaseProvider]] implementation providing Graal Community Edition
|
||||
* releases from the given [[ReleaseProvider]].
|
||||
* releases from the given [[SimpleReleaseProvider]].
|
||||
*/
|
||||
class GraalCEReleaseProvider(releaseProvider: ReleaseProvider)
|
||||
class GraalCEReleaseProvider(releaseProvider: SimpleReleaseProvider)
|
||||
extends RuntimeReleaseProvider {
|
||||
|
||||
/**
|
@ -1,4 +1,4 @@
|
||||
package org.enso.launcher.releases
|
||||
package org.enso.launcher.releases.runtime
|
||||
|
||||
import java.nio.file.Path
|
||||
|
@ -1,4 +1,4 @@
|
||||
package org.enso.launcher.components
|
||||
package org.enso.launcher.releases.testing
|
||||
|
||||
import java.nio.file.{Files, Path, StandardCopyOption}
|
||||
|
||||
@ -6,8 +6,8 @@ import org.enso.cli.{ProgressListener, TaskProgress}
|
||||
import org.enso.launcher.releases.{
|
||||
Asset,
|
||||
Release,
|
||||
ReleaseProvider,
|
||||
ReleaseProviderException
|
||||
ReleaseProviderException,
|
||||
SimpleReleaseProvider
|
||||
}
|
||||
import org.enso.launcher.{FileSystem, OS}
|
||||
|
||||
@ -16,7 +16,7 @@ import scala.sys.process._
|
||||
import scala.util.{Success, Try, Using}
|
||||
|
||||
/**
|
||||
* A release provider that creates fake releases from the defined resources.
|
||||
* A release provider that creates fake releases from the specified files.
|
||||
*
|
||||
* @param releasesRoot path to the directory containing subdirectories for each
|
||||
* release
|
||||
@ -26,7 +26,7 @@ import scala.util.{Success, Try, Using}
|
||||
case class FakeReleaseProvider(
|
||||
releasesRoot: Path,
|
||||
copyIntoArchiveRoot: Seq[String] = Seq.empty
|
||||
) extends ReleaseProvider {
|
||||
) extends SimpleReleaseProvider {
|
||||
private val releases =
|
||||
FileSystem
|
||||
.listDirectory(releasesRoot)
|
@ -0,0 +1,419 @@
|
||||
package org.enso.launcher.upgrade
|
||||
|
||||
import java.nio.file.{Files, Path}
|
||||
|
||||
import nl.gn0s1s.bump.SemVer
|
||||
import org.enso.cli.CLIOutput
|
||||
import org.enso.launcher.FileSystem.PathSyntax
|
||||
import org.enso.launcher.archive.Archive
|
||||
import org.enso.launcher.cli.{GlobalCLIOptions, InternalOpts}
|
||||
import org.enso.launcher.components.LauncherUpgradeRequiredError
|
||||
import org.enso.launcher.installation.DistributionManager
|
||||
import org.enso.launcher.releases.launcher.LauncherRelease
|
||||
import org.enso.launcher.releases.{EnsoRepository, ReleaseProvider}
|
||||
import org.enso.launcher.{CurrentVersion, FileSystem, Logger, OS}
|
||||
|
||||
import scala.util.Try
|
||||
import scala.util.control.NonFatal
|
||||
|
||||
class LauncherUpgrader(
|
||||
globalCLIOptions: GlobalCLIOptions,
|
||||
distributionManager: DistributionManager,
|
||||
releaseProvider: ReleaseProvider[LauncherRelease],
|
||||
originalExecutablePath: Option[Path]
|
||||
) {
|
||||
|
||||
/**
|
||||
* Queries the release provider for the latest available valid launcher
|
||||
* version.
|
||||
*/
|
||||
def latestVersion(): Try[SemVer] = {
|
||||
releaseProvider.findLatestVersion()
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs an upgrade to the `targetVersion`.
|
||||
*
|
||||
* The upgrade may first temporarily install versions older than the target
|
||||
* if the upgrade cannot be performed directly from the current version.
|
||||
*/
|
||||
def upgrade(targetVersion: SemVer): Unit = {
|
||||
runCleanup(isStartup = true)
|
||||
val release = releaseProvider.fetchRelease(targetVersion).get
|
||||
if (release.isMarkedBroken) {
|
||||
if (globalCLIOptions.autoConfirm) {
|
||||
Logger.warn(
|
||||
s"The launcher release $targetVersion is marked as broken and it " +
|
||||
s"should not be used. Since `auto-confirm` is set, the upgrade " +
|
||||
s"will continue, but you may want to reconsider upgrading to a " +
|
||||
s"stable release."
|
||||
)
|
||||
} else {
|
||||
Logger.warn(
|
||||
s"The launcher release $targetVersion is marked as broken and it " +
|
||||
s"should not be used."
|
||||
)
|
||||
val continue = CLIOutput.askConfirmation(
|
||||
"Are you sure you still want to continue upgrading to this version " +
|
||||
"despite the warning?"
|
||||
)
|
||||
if (!continue) {
|
||||
throw UpgradeError(
|
||||
"Upgrade has been cancelled by the user because the requested " +
|
||||
"version is marked as broken."
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (release.canPerformUpgradeFromCurrentVersion)
|
||||
performUpgradeTo(release)
|
||||
else
|
||||
performStepByStepUpgrade(release)
|
||||
|
||||
runCleanup()
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleans up temporary and old launcher executables.
|
||||
*
|
||||
* Some executables may fail to be cleaned the first time, if other launcher
|
||||
* instances are still running. To ensure that old executables are cleaned,
|
||||
* this method can be run at launcher startup.
|
||||
*
|
||||
* @param isStartup specifies if the run is at startup; it will display a
|
||||
* message informing about the cleanup in this case
|
||||
*/
|
||||
def runCleanup(isStartup: Boolean = false): Unit = {
|
||||
val binRoot = originalExecutable.getParent
|
||||
val temporaryFiles =
|
||||
FileSystem.listDirectory(binRoot).filter(isTemporaryExecutable)
|
||||
if (temporaryFiles.nonEmpty && isStartup) {
|
||||
Logger.debug("Cleaning temporary files from a previous upgrade.")
|
||||
}
|
||||
for (file <- temporaryFiles) {
|
||||
try {
|
||||
Files.delete(file)
|
||||
Logger.debug(s"Upgrade cleanup: removed `$file`.")
|
||||
} catch {
|
||||
case NonFatal(e) =>
|
||||
Logger.debug(s"Cannot remove temporary file $file: $e", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Continues a multi-step upgrade.
|
||||
*
|
||||
* Called by [[InternalOpts]] when the upgrade continuation is requested by
|
||||
* [[runNextUpgradeStep]].
|
||||
*/
|
||||
def internalContinueUpgrade(targetVersion: SemVer): Unit = {
|
||||
val release = releaseProvider.fetchRelease(targetVersion).get
|
||||
if (release.canPerformUpgradeFromCurrentVersion)
|
||||
performUpgradeTo(release)
|
||||
else
|
||||
performStepByStepUpgrade(release)
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the next step of the upgrade using the newly extracted newer launcher
|
||||
* version.
|
||||
*
|
||||
* @param temporaryExecutable path to the new, temporary launcher executable
|
||||
* @param targetVersion version to upgrade to
|
||||
*/
|
||||
private def runNextUpgradeStep(
|
||||
temporaryExecutable: Path,
|
||||
targetVersion: SemVer
|
||||
): Unit = {
|
||||
val exitCode = InternalOpts
|
||||
.runWithNewLauncher(temporaryExecutable)
|
||||
.continueUpgrade(
|
||||
targetVersion = targetVersion,
|
||||
originalPath = originalExecutable,
|
||||
globalCLIOptions = globalCLIOptions
|
||||
)
|
||||
if (exitCode != 0) {
|
||||
throw UpgradeError("Next upgrade step has failed. Upgrade cancelled.")
|
||||
}
|
||||
}
|
||||
|
||||
private def showProgress = !globalCLIOptions.hideProgress
|
||||
|
||||
/**
|
||||
* Path to the original launcher executable.
|
||||
*/
|
||||
private val originalExecutable =
|
||||
originalExecutablePath.getOrElse(
|
||||
distributionManager.env.getPathToRunningExecutable
|
||||
)
|
||||
|
||||
/**
|
||||
* Performs a step-by-step recursive upgrade.
|
||||
*
|
||||
* Finds a next version that can be directly upgraded to and is newer enough
|
||||
* to allow to upgrade to new versions, extracts it and runs it telling it to
|
||||
* continue upgrading to the target version. The extracted version may
|
||||
* download additional versions if more steps are needed.
|
||||
*
|
||||
* @param release release associated with the target version
|
||||
*/
|
||||
private def performStepByStepUpgrade(release: LauncherRelease): Unit = {
|
||||
val availableVersions = releaseProvider.fetchAllValidVersions().get
|
||||
val nextStepRelease = nextVersionToUpgradeTo(release, availableVersions)
|
||||
Logger.info(
|
||||
s"Cannot upgrade to ${release.version} directly, " +
|
||||
s"so a multiple-step upgrade will be performed, first upgrading to " +
|
||||
s"${nextStepRelease.version}."
|
||||
)
|
||||
|
||||
val temporaryExecutable = temporaryExecutablePath(
|
||||
"new." + nextStepRelease.version.toString
|
||||
)
|
||||
FileSystem.withTemporaryDirectory("enso-upgrade-step") { directory =>
|
||||
Logger.info(s"Downloading ${nextStepRelease.packageFileName}.")
|
||||
val packagePath = directory / nextStepRelease.packageFileName
|
||||
nextStepRelease
|
||||
.downloadPackage(packagePath)
|
||||
.waitForResult(showProgress)
|
||||
.get
|
||||
|
||||
Logger.info(
|
||||
s"Extracting the executable from ${nextStepRelease.packageFileName}."
|
||||
)
|
||||
extractExecutable(packagePath, temporaryExecutable)
|
||||
|
||||
Logger.info(
|
||||
s"Upgraded to ${nextStepRelease.version}. " +
|
||||
s"Proceeding to the next step of the upgrade."
|
||||
)
|
||||
runNextUpgradeStep(temporaryExecutable, release.version)
|
||||
}
|
||||
}
|
||||
|
||||
@scala.annotation.tailrec
|
||||
private def nextVersionToUpgradeTo(
|
||||
release: LauncherRelease,
|
||||
availableVersions: Seq[SemVer]
|
||||
): LauncherRelease = {
|
||||
val recentEnoughVersions =
|
||||
availableVersions.filter(_ >= release.minimumVersionToPerformUpgrade)
|
||||
val minimumValidVersion = recentEnoughVersions.sorted.headOption.getOrElse {
|
||||
throw UpgradeError(
|
||||
s"Upgrade failed: To continue upgrade, a version at least " +
|
||||
s"${release.minimumVersionToPerformUpgrade} is required, but no " +
|
||||
s"valid version satisfying this requirement could be found."
|
||||
)
|
||||
}
|
||||
val nextRelease = releaseProvider.fetchRelease(minimumValidVersion).get
|
||||
Logger.debug(
|
||||
s"To upgrade to ${release.version}, " +
|
||||
s"the launcher will have to upgrade to ${nextRelease.version} first."
|
||||
)
|
||||
if (nextRelease.canPerformUpgradeFromCurrentVersion)
|
||||
nextRelease
|
||||
else nextVersionToUpgradeTo(nextRelease, availableVersions)
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts just the launcher executable from the archive.
|
||||
*
|
||||
* @param archivePath path to the archive
|
||||
* @param executablePath path where to put the extracted executable
|
||||
*/
|
||||
private def extractExecutable(
|
||||
archivePath: Path,
|
||||
executablePath: Path
|
||||
): Unit = {
|
||||
var entryFound = false
|
||||
Archive
|
||||
.iterateArchive(archivePath) { entry =>
|
||||
if (
|
||||
entry.relativePath.endsWith(
|
||||
Path.of("bin") / OS.executableName("enso")
|
||||
)
|
||||
) {
|
||||
entryFound = true
|
||||
entry.extractTo(executablePath)
|
||||
false
|
||||
} else true
|
||||
}
|
||||
.waitForResult(showProgress)
|
||||
.get
|
||||
if (!entryFound) {
|
||||
throw UpgradeError(
|
||||
s"Launcher executable was not found in `$archivePath`."
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private val temporaryExecutablePrefix = "enso.tmp."
|
||||
|
||||
private def isTemporaryExecutable(path: Path): Boolean =
|
||||
path.getFileName.toString.startsWith(temporaryExecutablePrefix)
|
||||
|
||||
private def temporaryExecutablePath(suffix: String): Path = {
|
||||
val newName = OS.executableName(temporaryExecutablePrefix + suffix)
|
||||
val binRoot = originalExecutable.getParent
|
||||
binRoot / newName
|
||||
}
|
||||
|
||||
private def copyNonEssentialFiles(
|
||||
extractedRoot: Path,
|
||||
release: LauncherRelease
|
||||
): Unit =
|
||||
try {
|
||||
val dataRoot = distributionManager.paths.dataRoot
|
||||
for (file <- release.manifest.filesToCopy) {
|
||||
FileSystem.copyFile(
|
||||
extractedRoot / file,
|
||||
dataRoot / file
|
||||
)
|
||||
}
|
||||
for (dir <- release.manifest.directoriesToCopy) {
|
||||
val destination = dataRoot / dir
|
||||
FileSystem.removeDirectoryIfExists(destination)
|
||||
FileSystem.copyDirectory(extractedRoot / dir, destination)
|
||||
}
|
||||
} catch {
|
||||
case NonFatal(e) =>
|
||||
Logger.error(
|
||||
"An error occurred when copying one of the non-crucial files and " +
|
||||
"directories. The upgrade will continue, but the README or " +
|
||||
"licences may be out of date.",
|
||||
e
|
||||
)
|
||||
}
|
||||
|
||||
private def performUpgradeTo(release: LauncherRelease): Unit = {
|
||||
FileSystem.withTemporaryDirectory("enso-upgrade") { directory =>
|
||||
Logger.info(s"Downloading ${release.packageFileName}.")
|
||||
val packagePath = directory / release.packageFileName
|
||||
release.downloadPackage(packagePath).waitForResult(showProgress).get
|
||||
|
||||
Logger.info("Extracting package.")
|
||||
Archive
|
||||
.extractArchive(packagePath, directory, None)
|
||||
.waitForResult(showProgress)
|
||||
.get
|
||||
val extractedRoot = directory / "enso"
|
||||
|
||||
val temporaryExecutable = temporaryExecutablePath("new")
|
||||
FileSystem.copyFile(
|
||||
extractedRoot / "bin" / OS.executableName("enso"),
|
||||
temporaryExecutable
|
||||
)
|
||||
|
||||
copyNonEssentialFiles(extractedRoot, release)
|
||||
|
||||
Logger.info("Replacing the old launcher executable with the new one.")
|
||||
replaceLauncherExecutable(temporaryExecutable)
|
||||
|
||||
val verb =
|
||||
if (release.version >= CurrentVersion.version) "upgraded"
|
||||
else "downgraded"
|
||||
Logger.info(s"Successfully $verb launcher to ${release.version}.")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces the current launcher executable with a new one.
|
||||
*
|
||||
* On UNIX systems, it just removes the old one and moves the new one in its
|
||||
* place.
|
||||
*
|
||||
* On Windows, the currently running executable cannot be deleted, so instead
|
||||
* it is renamed to a different name, so that the new one can be moved in its
|
||||
* place. The old executable is removed later when cleanup is run.
|
||||
*
|
||||
* @param newExecutable path to the new executable that will replace the old
|
||||
* one
|
||||
*/
|
||||
private def replaceLauncherExecutable(newExecutable: Path): Unit = {
|
||||
Logger.debug(s"Replacing $originalExecutable with $newExecutable")
|
||||
if (OS.isWindows) {
|
||||
val oldName = temporaryExecutablePath(s"old-${CurrentVersion.version}")
|
||||
Files.move(originalExecutable, oldName)
|
||||
Files.move(newExecutable, originalExecutable)
|
||||
} else {
|
||||
Files.delete(originalExecutable)
|
||||
Files.move(newExecutable, originalExecutable)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
object LauncherUpgrader {
|
||||
|
||||
/**
|
||||
* Creates a [[LauncherUpgrader]] using the default [[DistributionManager]]
|
||||
* and release providers.
|
||||
*
|
||||
* Should be run late enough so that the testing repository override can be
|
||||
* applied. It is enough to run it inside of the standard options parsing.
|
||||
*
|
||||
* @param globalCLIOptions options from the CLI setting verbosity of the
|
||||
* executed actions
|
||||
* @param originalExecutablePath specifies the path of the original launcher
|
||||
* executable that will be replaced in the last
|
||||
* step of the upgrade
|
||||
*/
|
||||
def makeDefault(
|
||||
globalCLIOptions: GlobalCLIOptions,
|
||||
originalExecutablePath: Option[Path] = None
|
||||
): LauncherUpgrader =
|
||||
new LauncherUpgrader(
|
||||
globalCLIOptions,
|
||||
DistributionManager,
|
||||
EnsoRepository.defaultLauncherReleaseProvider,
|
||||
originalExecutablePath
|
||||
)
|
||||
|
||||
def recoverUpgradeRequiredErrors(originalArguments: Array[String])(
|
||||
action: => Int
|
||||
): Int = {
|
||||
try {
|
||||
action
|
||||
} catch {
|
||||
case e: LauncherUpgradeRequiredError =>
|
||||
val autoConfirm = e.globalCLIOptions.autoConfirm
|
||||
def shouldProceed: Boolean =
|
||||
if (autoConfirm) {
|
||||
Logger.warn(
|
||||
"A more recent launcher version is required. Since " +
|
||||
"`auto-confirm` is set, the launcher upgrade will be peformed " +
|
||||
"automatically."
|
||||
)
|
||||
true
|
||||
} else {
|
||||
Logger.warn("A more recent launcher version is required.")
|
||||
CLIOutput.askConfirmation(
|
||||
"Do you want to upgrade the launcher and continue?",
|
||||
yesDefault = true
|
||||
)
|
||||
}
|
||||
|
||||
if (!shouldProceed) {
|
||||
throw e
|
||||
}
|
||||
|
||||
val upgrader = makeDefault(e.globalCLIOptions)
|
||||
val targetVersion = upgrader.latestVersion().get
|
||||
val launcherExecutable = upgrader.originalExecutable
|
||||
upgrader.upgrade(targetVersion)
|
||||
|
||||
Logger.info(
|
||||
"Re-running the current command with the upgraded launcher."
|
||||
)
|
||||
|
||||
val arguments =
|
||||
InternalOpts.removeInternalTestOptions(originalArguments.toIndexedSeq)
|
||||
val rerunCommand =
|
||||
Seq(launcherExecutable.toAbsolutePath.normalize.toString) ++ arguments
|
||||
Logger.debug(s"Running `${rerunCommand.mkString(" ")}`.")
|
||||
val processBuilder = new ProcessBuilder(rerunCommand: _*)
|
||||
val process = processBuilder.inheritIO().start()
|
||||
process.waitFor()
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
package org.enso.launcher.upgrade
|
||||
|
||||
/**
|
||||
* Indicates an error during an upgrade.
|
||||
*/
|
||||
case class UpgradeError(message: String, cause: Throwable = null)
|
||||
extends RuntimeException(message, cause) {
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
override def toString: String = message
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
minimum-version-for-upgrade: 0.0.0
|
||||
files-to-copy: []
|
||||
directories-to-copy: []
|
@ -0,0 +1 @@
|
||||
Content
|
@ -0,0 +1 @@
|
||||
Test license
|
@ -0,0 +1 @@
|
||||
Content
|
@ -0,0 +1 @@
|
||||
Test license
|
@ -0,0 +1 @@
|
||||
Content
|
@ -0,0 +1 @@
|
||||
Test license
|
@ -0,0 +1,5 @@
|
||||
minimum-version-for-upgrade: 0.0.0
|
||||
files-to-copy:
|
||||
- README.md
|
||||
directories-to-copy:
|
||||
- components-licences
|
@ -0,0 +1,3 @@
|
||||
minimum-version-for-upgrade: 0.0.1
|
||||
files-to-copy: []
|
||||
directories-to-copy: []
|
@ -0,0 +1,3 @@
|
||||
minimum-version-for-upgrade: 0.0.2
|
||||
files-to-copy: []
|
||||
directories-to-copy: []
|
@ -0,0 +1,3 @@
|
||||
minimum-version-for-upgrade: 0.0.2
|
||||
files-to-copy: []
|
||||
directories-to-copy: []
|
@ -0,0 +1,3 @@
|
||||
minimum-version-for-upgrade: 0.0.2
|
||||
files-to-copy: []
|
||||
directories-to-copy: []
|
@ -0,0 +1,21 @@
|
||||
package org.enso.launcher
|
||||
|
||||
import java.nio.file.Path
|
||||
|
||||
import scala.io.Source
|
||||
import scala.util.Using
|
||||
|
||||
/**
|
||||
* Gathers helper functions for the test suite.
|
||||
*/
|
||||
object TestHelpers {
|
||||
|
||||
/**
|
||||
* Reads file contents into a [[String]].
|
||||
*/
|
||||
def readFileContent(path: Path): String = {
|
||||
Using(Source.fromFile(path.toFile)) { source =>
|
||||
source.getLines().mkString("\n")
|
||||
}.get
|
||||
}
|
||||
}
|
@ -71,7 +71,7 @@ class ComponentsManagerSpec extends ComponentsManagerTest {
|
||||
"preserve the broken mark when installing a broken release" in {
|
||||
Logger.suppressWarnings {
|
||||
val componentsManager = makeComponentsManager()
|
||||
val brokenVersion = SemVer(0, 1, 0, Some("marked-broken"))
|
||||
val brokenVersion = SemVer(0, 999, 0, Some("marked-broken"))
|
||||
componentsManager.findOrInstallEngine(
|
||||
brokenVersion,
|
||||
complain = false
|
||||
@ -92,7 +92,7 @@ class ComponentsManagerSpec extends ComponentsManagerTest {
|
||||
new GlobalConfigurationManager(componentsManager, distributionManager)
|
||||
|
||||
val validVersion = SemVer(0, 0, 1)
|
||||
val newerButBrokenVersion = SemVer(0, 1, 0, Some("marked-broken"))
|
||||
val newerButBrokenVersion = SemVer(0, 999, 0, Some("marked-broken"))
|
||||
componentsManager.findOrInstallEngine(validVersion)
|
||||
componentsManager.findOrInstallEngine(newerButBrokenVersion)
|
||||
|
||||
@ -105,7 +105,7 @@ class ComponentsManagerSpec extends ComponentsManagerTest {
|
||||
Console.withErr(stream) {
|
||||
val componentsManager = makeComponentsManager()
|
||||
|
||||
val brokenVersion = SemVer(0, 1, 0, Some("marked-broken"))
|
||||
val brokenVersion = SemVer(0, 999, 0, Some("marked-broken"))
|
||||
componentsManager.findOrInstallEngine(brokenVersion)
|
||||
|
||||
componentsManager.findEngine(brokenVersion).value
|
||||
|
@ -5,10 +5,9 @@ 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.releases.engine.EngineReleaseProvider
|
||||
import org.enso.launcher.releases.runtime.GraalCEReleaseProvider
|
||||
import org.enso.launcher.releases.testing.FakeReleaseProvider
|
||||
import org.enso.launcher.{Environment, FakeEnvironment, WithTemporaryDirectory}
|
||||
import org.enso.pkg.{PackageManager, SemVerEnsoVersion}
|
||||
import org.scalatest.OptionValues
|
||||
|
@ -2,10 +2,8 @@ package org.enso.launcher.installation
|
||||
|
||||
import java.nio.file.{Files, Path}
|
||||
|
||||
import org.enso.launcher.{FileSystem, NativeTest, OS, WithTemporaryDirectory}
|
||||
import org.enso.launcher.FileSystem.PathSyntax
|
||||
|
||||
import scala.io.Source
|
||||
import org.enso.launcher._
|
||||
|
||||
class InstallerSpec extends NativeTest with WithTemporaryDirectory {
|
||||
def portableRoot = getTestDirectory / "portable"
|
||||
@ -41,15 +39,6 @@ class InstallerSpec extends NativeTest with WithTemporaryDirectory {
|
||||
FileSystem.writeTextFile(runtimeBundle / "jvm.txt", "")
|
||||
}
|
||||
|
||||
def readFileContent(path: Path): String = {
|
||||
val source = Source.fromFile(path.toFile)
|
||||
try {
|
||||
source.getLines().mkString("\n")
|
||||
} finally {
|
||||
source.close()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the file does not exist, retrying `retry` times with a 200ms
|
||||
* delay between retries.
|
||||
@ -82,7 +71,9 @@ class InstallerSpec extends NativeTest with WithTemporaryDirectory {
|
||||
|
||||
val config = installedRoot / "config" / "global-config.yaml"
|
||||
config.toFile should exist
|
||||
readFileContent(config).stripTrailing() shouldEqual "what: ever"
|
||||
TestHelpers
|
||||
.readFileContent(config)
|
||||
.stripTrailing() shouldEqual "what: ever"
|
||||
|
||||
assert(
|
||||
Files.notExists(portableLauncher),
|
||||
|
@ -0,0 +1,279 @@
|
||||
package org.enso.launcher.upgrade
|
||||
|
||||
import java.nio.file.{Files, Path, StandardCopyOption}
|
||||
|
||||
import io.circe.parser
|
||||
import nl.gn0s1s.bump.SemVer
|
||||
import org.enso.launcher.FileSystem.PathSyntax
|
||||
import org.enso.launcher._
|
||||
import org.scalatest.exceptions.TestFailedException
|
||||
import org.scalatest.{BeforeAndAfterAll, OptionValues}
|
||||
|
||||
class UpgradeSpec
|
||||
extends NativeTest
|
||||
with WithTemporaryDirectory
|
||||
with BeforeAndAfterAll
|
||||
with OptionValues {
|
||||
|
||||
/**
|
||||
* Location of the fake releases root.
|
||||
*/
|
||||
private val fakeReleaseRoot = Path
|
||||
.of(
|
||||
getClass
|
||||
.getResource("/org/enso/launcher/components/fake-releases")
|
||||
.toURI
|
||||
) / "launcher"
|
||||
|
||||
/**
|
||||
* Location of built Rust artifacts.
|
||||
*/
|
||||
private val rustBuildRoot = Path.of("./target/rust/debug/")
|
||||
|
||||
/**
|
||||
* Location of the actual launcher executable that is wrapped by the shims.
|
||||
*/
|
||||
private val realLauncherLocation =
|
||||
Path.of(".").resolve(OS.executableName("enso")).toAbsolutePath.normalize
|
||||
|
||||
/**
|
||||
* Path to a launcher shim that pretends to be `version`.
|
||||
*/
|
||||
private def builtLauncherBinary(version: SemVer): Path = {
|
||||
val simplifiedVersion = version.toString.replaceAll("[.-]", "")
|
||||
rustBuildRoot / OS.executableName(s"launcher_$simplifiedVersion")
|
||||
}
|
||||
|
||||
/**
|
||||
* Copies a launcher shim into the fake release directory.
|
||||
*/
|
||||
private def prepareLauncherBinary(version: SemVer): Unit = {
|
||||
val os = OS.operatingSystem.configName
|
||||
val arch = OS.architecture
|
||||
val ext = if (OS.isWindows) "zip" else "tar.gz"
|
||||
val packageName = s"enso-launcher-$version-$os-$arch.$ext"
|
||||
val destinationDirectory =
|
||||
fakeReleaseRoot / s"enso-$version" / packageName / "enso" / "bin"
|
||||
Files.createDirectories(destinationDirectory)
|
||||
Files.copy(
|
||||
builtLauncherBinary(version),
|
||||
destinationDirectory / OS.executableName("enso"),
|
||||
StandardCopyOption.REPLACE_EXISTING
|
||||
)
|
||||
}
|
||||
|
||||
override def beforeAll(): Unit = {
|
||||
super.beforeAll()
|
||||
prepareLauncherBinary(SemVer(0, 0, 0))
|
||||
prepareLauncherBinary(SemVer(0, 0, 1))
|
||||
prepareLauncherBinary(SemVer(0, 0, 2))
|
||||
prepareLauncherBinary(SemVer(0, 0, 3))
|
||||
prepareLauncherBinary(SemVer(0, 0, 4))
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares a launcher distribution in the temporary test location.
|
||||
*
|
||||
* If `launcherVersion` is not provided, the default one is used.
|
||||
*
|
||||
* It waits a 100ms delay after creating the launcher copy to ensure that the
|
||||
* copy can be called right away after calling this function. It is not
|
||||
* absolutely certain that this is helpful, but from time to time, the tests
|
||||
* fail because the filesystem does not allow to access the executable as
|
||||
* 'not-ready'. This delay is an attempt to make the tests more stable.
|
||||
*/
|
||||
private def prepareDistribution(
|
||||
portable: Boolean,
|
||||
launcherVersion: Option[SemVer] = None
|
||||
): Unit = {
|
||||
val sourceLauncherLocation =
|
||||
launcherVersion.map(builtLauncherBinary).getOrElse(baseLauncherLocation)
|
||||
Files.createDirectories(launcherPath.getParent)
|
||||
Files.copy(sourceLauncherLocation, launcherPath)
|
||||
if (portable) {
|
||||
val root = launcherPath.getParent.getParent
|
||||
FileSystem.writeTextFile(root / ".enso.portable", "mark")
|
||||
}
|
||||
Thread.sleep(100)
|
||||
}
|
||||
|
||||
/**
|
||||
* Path to the launcher executable in the temporary distribution.
|
||||
*/
|
||||
private def launcherPath =
|
||||
getTestDirectory / "enso" / "bin" / OS.executableName("enso")
|
||||
|
||||
/**
|
||||
* Runs `enso version` to inspect the version reported by the launcher.
|
||||
* @return the reported version
|
||||
*/
|
||||
private def checkVersion(): SemVer = {
|
||||
val result = run(
|
||||
Seq("version", "--json", "--only-launcher")
|
||||
)
|
||||
result should returnSuccess
|
||||
val version = parser.parse(result.stdout).getOrElse {
|
||||
throw new TestFailedException(
|
||||
s"Version should be a valid JSON string, got '${result.stdout}' " +
|
||||
s"instead.",
|
||||
1
|
||||
)
|
||||
}
|
||||
SemVer(version.asObject.value.apply("version").value.asString.value).value
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the launcher in the temporary distribution.
|
||||
*
|
||||
* @param args arguments for the launcher
|
||||
* @param extraEnv environment variable overrides
|
||||
* @return result of the run
|
||||
*/
|
||||
private def run(
|
||||
args: Seq[String],
|
||||
extraEnv: Map[String, String] = Map.empty
|
||||
): RunResult = {
|
||||
val testArgs = Seq(
|
||||
"--internal-emulate-repository",
|
||||
fakeReleaseRoot.toAbsolutePath.toString,
|
||||
"--auto-confirm",
|
||||
"--hide-progress"
|
||||
)
|
||||
val env =
|
||||
extraEnv.updated("ENSO_LAUNCHER_LOCATION", realLauncherLocation.toString)
|
||||
runLauncherAt(launcherPath, testArgs ++ args, env)
|
||||
}
|
||||
|
||||
"upgrade" should {
|
||||
"upgrade to latest version (excluding broken)" in {
|
||||
prepareDistribution(
|
||||
portable = true,
|
||||
launcherVersion = Some(SemVer(0, 0, 2))
|
||||
)
|
||||
run(Seq("upgrade")) should returnSuccess
|
||||
|
||||
checkVersion() shouldEqual SemVer(0, 0, 4)
|
||||
}
|
||||
|
||||
"not downgrade without being explicitly asked to do so" in {
|
||||
// precondition for the test to make sense
|
||||
SemVer(buildinfo.Info.ensoVersion).value should be > SemVer(0, 0, 4)
|
||||
|
||||
prepareDistribution(
|
||||
portable = true
|
||||
)
|
||||
run(Seq("upgrade")).exitCode shouldEqual 1
|
||||
}
|
||||
|
||||
"upgrade/downgrade to a specific version " +
|
||||
"(and update necessary files)" in {
|
||||
// precondition for the test to make sense
|
||||
SemVer(buildinfo.Info.ensoVersion).value should be > SemVer(0, 0, 4)
|
||||
|
||||
prepareDistribution(
|
||||
portable = true
|
||||
)
|
||||
val root = launcherPath.getParent.getParent
|
||||
FileSystem.writeTextFile(root / "README.md", "Old readme")
|
||||
run(Seq("upgrade", "0.0.1")) should returnSuccess
|
||||
checkVersion() shouldEqual SemVer(0, 0, 1)
|
||||
TestHelpers.readFileContent(root / "README.md").trim shouldEqual "Content"
|
||||
TestHelpers
|
||||
.readFileContent(root / "components-licences" / "test-license.txt")
|
||||
.trim shouldEqual "Test license"
|
||||
}
|
||||
|
||||
"upgrade also in installed mode" in {
|
||||
prepareDistribution(
|
||||
portable = false,
|
||||
launcherVersion = Some(SemVer(0, 0, 0))
|
||||
)
|
||||
val dataRoot = getTestDirectory / "data"
|
||||
val configRoot = getTestDirectory / "config"
|
||||
checkVersion() shouldEqual SemVer(0, 0, 0)
|
||||
val env = Map(
|
||||
"ENSO_DATA_DIRECTORY" -> dataRoot.toString,
|
||||
"ENSO_CONFIG_DIRECTORY" -> configRoot.toString
|
||||
)
|
||||
run(
|
||||
Seq("upgrade", "0.0.1"),
|
||||
extraEnv = env
|
||||
) should returnSuccess
|
||||
checkVersion() shouldEqual SemVer(0, 0, 1)
|
||||
TestHelpers
|
||||
.readFileContent(dataRoot / "README.md")
|
||||
.trim shouldEqual "Content"
|
||||
TestHelpers
|
||||
.readFileContent(dataRoot / "components-licences" / "test-license.txt")
|
||||
.trim shouldEqual "Test license"
|
||||
}
|
||||
|
||||
"perform a multi-step upgrade if necessary" in {
|
||||
// 0.0.3 can only be upgraded from 0.0.2 which can only be upgraded from
|
||||
// 0.0.1, so the upgrade path should be following:
|
||||
// 0.0.0 -> 0.0.1 -> 0.0.2 -> 0.0.3
|
||||
prepareDistribution(
|
||||
portable = true,
|
||||
launcherVersion = Some(SemVer(0, 0, 0))
|
||||
)
|
||||
|
||||
checkVersion() shouldEqual SemVer(0, 0, 0)
|
||||
run(Seq("upgrade", "0.0.3")) should returnSuccess
|
||||
checkVersion() shouldEqual SemVer(0, 0, 3)
|
||||
|
||||
val launchedVersions = Seq(
|
||||
"0.0.0",
|
||||
"0.0.0",
|
||||
"0.0.1",
|
||||
"0.0.2",
|
||||
"0.0.3"
|
||||
)
|
||||
|
||||
val reportedLaunchLog = TestHelpers
|
||||
.readFileContent(launcherPath.getParent / ".launcher_version_log")
|
||||
.trim
|
||||
.linesIterator
|
||||
.toSeq
|
||||
|
||||
reportedLaunchLog shouldEqual launchedVersions
|
||||
}
|
||||
|
||||
"automatically trigger if an action requires a newer version and re-run " +
|
||||
"that action with the upgraded launcher" ignore {
|
||||
prepareDistribution(
|
||||
portable = true,
|
||||
launcherVersion = Some(SemVer(0, 0, 2))
|
||||
)
|
||||
val enginesPath = getTestDirectory / "enso" / "dist"
|
||||
Files.createDirectories(enginesPath)
|
||||
|
||||
// TODO [RW] re-enable this test when #1046 is done and the engine
|
||||
// distribution can be used in the test
|
||||
// FileSystem.copyDirectory(
|
||||
// Path.of("target/distribution/"),
|
||||
// enginesPath / "0.1.0"
|
||||
// )
|
||||
val script = getTestDirectory / "script.enso"
|
||||
val message = "Hello from test"
|
||||
val content =
|
||||
s"""from Builtins import all
|
||||
|main = IO.println "$message"
|
||||
|""".stripMargin
|
||||
FileSystem.writeTextFile(script, content)
|
||||
|
||||
// TODO [RW] make sure the right `java` is used to run the engine
|
||||
// (this should be dealt with in #1046)
|
||||
val result = run(
|
||||
Seq(
|
||||
"run",
|
||||
script.toAbsolutePath.toString,
|
||||
"--use-system-jvm",
|
||||
"--use-enso-version",
|
||||
"0.1.0"
|
||||
)
|
||||
)
|
||||
result should returnSuccess
|
||||
result.stdout should include(message)
|
||||
}
|
||||
}
|
||||
}
|
11
lib/rust/launcher-shims/Cargo.toml
Normal file
11
lib/rust/launcher-shims/Cargo.toml
Normal file
@ -0,0 +1,11 @@
|
||||
[package]
|
||||
name = "launcher-shims"
|
||||
version = "0.1.0"
|
||||
authors = ["Enso Team <enso-dev@enso.org>"]
|
||||
edition = "2018"
|
||||
|
||||
description = "Small wrappers for the launcher executable used for testing launcher upgrade."
|
||||
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
12
lib/rust/launcher-shims/src/bin/launcher_000.rs
Normal file
12
lib/rust/launcher-shims/src/bin/launcher_000.rs
Normal file
@ -0,0 +1,12 @@
|
||||
use launcher_shims::wrap_launcher;
|
||||
|
||||
|
||||
|
||||
// =======================
|
||||
// === EntryPoint0.0.0 ===
|
||||
// =======================
|
||||
|
||||
/// Runs the launcher wrapper overriding the version to 0.0.0.
|
||||
fn main() {
|
||||
wrap_launcher("0.0.0")
|
||||
}
|
12
lib/rust/launcher-shims/src/bin/launcher_001.rs
Normal file
12
lib/rust/launcher-shims/src/bin/launcher_001.rs
Normal file
@ -0,0 +1,12 @@
|
||||
use launcher_shims::wrap_launcher;
|
||||
|
||||
|
||||
|
||||
// =======================
|
||||
// === EntryPoint0.0.1 ===
|
||||
// =======================
|
||||
|
||||
/// Runs the launcher wrapper overriding the version to 0.0.1.
|
||||
fn main() {
|
||||
wrap_launcher("0.0.1")
|
||||
}
|
12
lib/rust/launcher-shims/src/bin/launcher_002.rs
Normal file
12
lib/rust/launcher-shims/src/bin/launcher_002.rs
Normal file
@ -0,0 +1,12 @@
|
||||
use launcher_shims::wrap_launcher;
|
||||
|
||||
|
||||
|
||||
// =======================
|
||||
// === EntryPoint0.0.2 ===
|
||||
// =======================
|
||||
|
||||
/// Runs the launcher wrapper overriding the version to 0.0.2.
|
||||
fn main() {
|
||||
wrap_launcher("0.0.2")
|
||||
}
|
12
lib/rust/launcher-shims/src/bin/launcher_003.rs
Normal file
12
lib/rust/launcher-shims/src/bin/launcher_003.rs
Normal file
@ -0,0 +1,12 @@
|
||||
use launcher_shims::wrap_launcher;
|
||||
|
||||
|
||||
|
||||
// =======================
|
||||
// === EntryPoint0.0.3 ===
|
||||
// =======================
|
||||
|
||||
/// Runs the launcher wrapper overriding the version to 0.0.3.
|
||||
fn main() {
|
||||
wrap_launcher("0.0.3")
|
||||
}
|
12
lib/rust/launcher-shims/src/bin/launcher_004.rs
Normal file
12
lib/rust/launcher-shims/src/bin/launcher_004.rs
Normal file
@ -0,0 +1,12 @@
|
||||
use launcher_shims::wrap_launcher;
|
||||
|
||||
|
||||
|
||||
// =======================
|
||||
// === EntryPoint0.0.4 ===
|
||||
// =======================
|
||||
|
||||
/// Runs the launcher wrapper overriding the version to 0.0.4.
|
||||
fn main() {
|
||||
wrap_launcher("0.0.4")
|
||||
}
|
78
lib/rust/launcher-shims/src/lib.rs
Normal file
78
lib/rust/launcher-shims/src/lib.rs
Normal file
@ -0,0 +1,78 @@
|
||||
use std::env;
|
||||
use std::fs::OpenOptions;
|
||||
use std::io;
|
||||
use std::io::prelude::*;
|
||||
use std::path::PathBuf;
|
||||
use std::process::{Command, exit};
|
||||
|
||||
|
||||
|
||||
// ====================
|
||||
// === WrapLauncher ===
|
||||
// ====================
|
||||
|
||||
/// Run the wrapped launcher overriding its reported version to the provided version.
|
||||
///
|
||||
/// The launcher's executable location is also overridden to point to this executable. The launcher
|
||||
/// is passed all the original arguments plus the arguments that handle the version and location
|
||||
/// override. The location of the original launcher executable that is wrapped is determined by the
|
||||
/// environment variable `ENSO_LAUNCHER_LOCATION` that should be set at build-time.
|
||||
///
|
||||
/// Additionally, the wrapper appends to a log file called `.launcher_version_log` a line containing
|
||||
/// the version string that was launched (creating the file if necessary). This can be used by tests
|
||||
/// to verify the order of launched versions.
|
||||
pub fn wrap_launcher(version:impl AsRef<str>) {
|
||||
let args: Vec<String> = env::args().collect();
|
||||
|
||||
let missing_location_message = "`ENSO_LAUNCHER_LOCATION` is not defined.";
|
||||
let launcher_location = env::var("ENSO_LAUNCHER_LOCATION").expect(missing_location_message);
|
||||
|
||||
let current_exe_path = env::current_exe().expect("Cannot get current executable path.");
|
||||
let exe_location = match current_exe_path.to_str() {
|
||||
Some(str) => str,
|
||||
None => {
|
||||
eprintln!("Path {} is invalid.", current_exe_path.to_string_lossy());
|
||||
exit(1)
|
||||
}
|
||||
};
|
||||
|
||||
let missing_directory_message = "Executable path should have a parent directory.";
|
||||
let parent_directory = current_exe_path.parent().expect(missing_directory_message);
|
||||
let log_name = ".launcher_version_log";
|
||||
let log_path = parent_directory.join(log_name);
|
||||
append_to_log(log_path, version.as_ref().to_string()).expect("Cannot write to log.");
|
||||
|
||||
let override_args = [
|
||||
String::from("--internal-emulate-version"),
|
||||
version.as_ref().to_string(),
|
||||
String::from("--internal-emulate-location"),
|
||||
String::from(exe_location)
|
||||
];
|
||||
let modified_args = [&override_args[..], &args[1..]].concat();
|
||||
|
||||
let exit_status = Command::new(launcher_location).args(modified_args).status();
|
||||
let exit_code = match exit_status {
|
||||
Ok (status) =>
|
||||
if let Some(code) = status.code() {
|
||||
code
|
||||
} else {
|
||||
eprintln!("Process terminated by signal.");
|
||||
exit(1)
|
||||
}
|
||||
Err(error) => {
|
||||
eprintln!("{}",error);
|
||||
exit(1)
|
||||
}
|
||||
};
|
||||
exit(exit_code)
|
||||
}
|
||||
|
||||
|
||||
// === Log ===
|
||||
|
||||
/// Appends a line to the file located at the provided path.
|
||||
pub fn append_to_log(path:PathBuf, line:impl AsRef<str>) -> io::Result<()> {
|
||||
let mut log_file = OpenOptions::new().create(true).write(true).append(true).open(path)?;
|
||||
writeln!(log_file,"{}",line.as_ref())?;
|
||||
Ok(())
|
||||
}
|
@ -2,6 +2,10 @@ package org.enso.version
|
||||
|
||||
import buildinfo.Info
|
||||
|
||||
/**
|
||||
* Represents a description of a version string that can be rendered as a
|
||||
* human-readable string or in JSON format.
|
||||
*/
|
||||
trait VersionDescription {
|
||||
def asHumanReadableString: String
|
||||
def asJSONString: String
|
||||
@ -27,22 +31,35 @@ case class VersionDescriptionParameter(
|
||||
)
|
||||
|
||||
object VersionDescription {
|
||||
def formatParameterAsJSONString(
|
||||
parameter: VersionDescriptionParameter
|
||||
): String =
|
||||
s""""${parameter.jsonName}": ${parameter.value}"""
|
||||
def formatParameterAsHumanReadableString(
|
||||
parameter: VersionDescriptionParameter
|
||||
): String =
|
||||
s"${parameter.humanReadableName}: ${parameter.value}"
|
||||
|
||||
/**
|
||||
* Creates a [[VersionDescription]] instance.
|
||||
*
|
||||
* @param header header displayed as the first line of the human-readable
|
||||
* representation
|
||||
* @param includeRuntimeJVMInfo if set to true, includes information about
|
||||
* the JVM that is running the program
|
||||
* @param enableNativeImageOSWorkaround if set to true, changes how the OS
|
||||
* information is displayed; this is a
|
||||
* temporary workaround caused by the
|
||||
* Native Image OS returning the value
|
||||
* known at build-time and not at
|
||||
* runtime
|
||||
* @param additionalParameters a sequence of additional
|
||||
* [[VersionDescriptionParameter]] to include in
|
||||
* the version string
|
||||
* @param customVersion if provided, overrides the version string from
|
||||
* [[buildinfo]]; used for testing purposes
|
||||
* @return
|
||||
*/
|
||||
def make(
|
||||
header: String,
|
||||
includeRuntimeJVMInfo: Boolean,
|
||||
enableNativeImageOSWorkaround: Boolean = false,
|
||||
additionalParameters: Seq[VersionDescriptionParameter] = Seq.empty
|
||||
additionalParameters: Seq[VersionDescriptionParameter] = Seq.empty,
|
||||
customVersion: Option[String] = None
|
||||
): VersionDescription = {
|
||||
val version = Info.ensoVersion
|
||||
val version = customVersion.getOrElse(Info.ensoVersion)
|
||||
val osArch = System.getProperty("os.arch")
|
||||
val osName = System.getProperty("os.name")
|
||||
val osVersion = System.getProperty("os.version")
|
||||
@ -106,4 +123,13 @@ object VersionDescription {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private def formatParameterAsJSONString(
|
||||
parameter: VersionDescriptionParameter
|
||||
): String =
|
||||
s""""${parameter.jsonName}": ${parameter.value}"""
|
||||
private def formatParameterAsHumanReadableString(
|
||||
parameter: VersionDescriptionParameter
|
||||
): String =
|
||||
s"${parameter.humanReadableName}: ${parameter.value}"
|
||||
}
|
||||
|
@ -4,6 +4,22 @@ import sbt.internal.util.ManagedLogger
|
||||
import scala.sys.process._
|
||||
|
||||
object BuildInfo {
|
||||
|
||||
/**
|
||||
* Writes build-time information to a Scala object that can be used by the
|
||||
* components.
|
||||
*
|
||||
* If the `ENSO_RELEASE_MODE` environment variable is set to `true`, will set
|
||||
* an `isRelease` flag to true. This flag can be used to disable
|
||||
* development-specific features.
|
||||
*
|
||||
* @param file location where to write the Scala code
|
||||
* @param log a logger instance for diagnostics
|
||||
* @param ensoVersion Enso version
|
||||
* @param scalacVersion Scala compiler version used in the project
|
||||
* @param graalVersion GraalVM version used in the project
|
||||
* @return sequence of modified files
|
||||
*/
|
||||
def writeBuildInfoFile(
|
||||
file: File,
|
||||
log: ManagedLogger,
|
||||
@ -11,7 +27,8 @@ object BuildInfo {
|
||||
scalacVersion: String,
|
||||
graalVersion: String
|
||||
): Seq[File] = {
|
||||
val gitInfo = getGitInformation(log).getOrElse(fallbackGitInformation)
|
||||
val gitInfo = getGitInformation(log).getOrElse(fallbackGitInformation)
|
||||
val isRelease = isReleaseMode
|
||||
val fileContents =
|
||||
s"""
|
||||
|package buildinfo
|
||||
@ -24,10 +41,14 @@ object BuildInfo {
|
||||
| val graalVersion = "$graalVersion"
|
||||
|
|
||||
| // Git Info
|
||||
| val commit = "${gitInfo.commit}"
|
||||
| val commit = "${gitInfo.commitHash}"
|
||||
| val ref = "${gitInfo.ref}"
|
||||
| val isDirty = ${gitInfo.isDirty}
|
||||
| val latestCommitDate = "${gitInfo.latestCommitDate}"
|
||||
|
|
||||
| // Release mode, set to true if the environment variable
|
||||
| // `ENSO_RELEASE_MODE` is set to `true` at build time.
|
||||
| val isRelease = $isRelease
|
||||
|}
|
||||
|""".stripMargin
|
||||
IO.write(file, fileContents)
|
||||
@ -35,12 +56,26 @@ object BuildInfo {
|
||||
Seq(file)
|
||||
}
|
||||
|
||||
private def isReleaseMode: Boolean =
|
||||
if (sys.env.get("ENSO_RELEASE_MODE").contains("true")) true else false
|
||||
|
||||
/**
|
||||
* Information regarding the Git repository that was used in the build.
|
||||
*
|
||||
* @param ref if available, name of the branch that was checked out; if a
|
||||
* branch is not available, but the current commit is tagged, name
|
||||
* of that tag is used, otherwise falls back to `HEAD`
|
||||
* @param commitHash hash of the currently checked out commit
|
||||
* @param isDirty indicates if there are any uncommitted changes
|
||||
* @param latestCommitDate date of the current commit
|
||||
*/
|
||||
private case class GitInformation(
|
||||
ref: String,
|
||||
commit: String,
|
||||
commitHash: String,
|
||||
isDirty: Boolean,
|
||||
latestCommitDate: String
|
||||
)
|
||||
|
||||
private def getGitInformation(log: ManagedLogger): Option[GitInformation] =
|
||||
try {
|
||||
val hash = ("git rev-parse HEAD" !!).trim
|
||||
@ -61,7 +96,14 @@ object BuildInfo {
|
||||
}
|
||||
val isDirty = !("git status --porcelain" !!).trim.isEmpty
|
||||
val latestCommitDate = ("git log HEAD -1 --format=%cd" !!).trim
|
||||
Some(GitInformation(ref, hash, isDirty, latestCommitDate))
|
||||
Some(
|
||||
GitInformation(
|
||||
ref = ref,
|
||||
commitHash = hash,
|
||||
isDirty = isDirty,
|
||||
latestCommitDate = latestCommitDate
|
||||
)
|
||||
)
|
||||
} catch {
|
||||
case e: Exception =>
|
||||
log.warn(
|
||||
@ -70,6 +112,12 @@ object BuildInfo {
|
||||
)
|
||||
None
|
||||
}
|
||||
|
||||
/**
|
||||
* Fallback instance of [[GitInformation]] that can be used if the build is
|
||||
* outside of a repository or the git information cannot be obtained for
|
||||
* other reasons.
|
||||
*/
|
||||
private def fallbackGitInformation: GitInformation =
|
||||
GitInformation(
|
||||
"<built outside of a git repository>",
|
||||
|
@ -4,8 +4,6 @@ import sbt.internal.util.ManagedLogger
|
||||
|
||||
import scala.sys.process._
|
||||
|
||||
|
||||
|
||||
/** A wrapper for executing the command `cargo`. */
|
||||
object Cargo {
|
||||
|
||||
@ -15,12 +13,26 @@ object Cargo {
|
||||
private val cargoCmd = "cargo"
|
||||
|
||||
/** Checks rust version and executes the command `cargo $args`. */
|
||||
def apply(args: String): Def.Initialize[Task[Unit]] = Def.task {
|
||||
run(args, rustVersion.value, state.value.log)
|
||||
}
|
||||
def apply(args: String): Def.Initialize[Task[Unit]] =
|
||||
Def.task {
|
||||
run(args, rustVersion.value, state.value.log)
|
||||
}
|
||||
|
||||
/** Checks rust version and executes the command `cargo $args`. */
|
||||
def run(args: String, rustVersion: String, log: ManagedLogger): Unit = {
|
||||
/**
|
||||
* Checks rust version and executes the command `cargo $args`.
|
||||
*
|
||||
* @param args arguments to pass to cargo
|
||||
* @param rustVersion Rust version that should be used
|
||||
* @param log a logger instance for diagnostics
|
||||
* @param extraEnv additional environment variables that should be set for
|
||||
* the cargo process
|
||||
*/
|
||||
def run(
|
||||
args: String,
|
||||
rustVersion: String,
|
||||
log: ManagedLogger,
|
||||
extraEnv: Seq[(String, String)] = Seq()
|
||||
): Unit = {
|
||||
val cmd = s"$cargoCmd $args"
|
||||
|
||||
if (!cargoOk(log))
|
||||
@ -31,15 +43,23 @@ object Cargo {
|
||||
|
||||
log.info(cmd)
|
||||
|
||||
try cmd.!! catch {
|
||||
case _: RuntimeException =>
|
||||
throw new RuntimeException("Cargo command failed.")
|
||||
val exitCode =
|
||||
try Process(cmd, None, extraEnv: _*).!
|
||||
catch {
|
||||
case _: RuntimeException =>
|
||||
throw new RuntimeException("Cargo command failed to run.")
|
||||
}
|
||||
if (exitCode != 0) {
|
||||
throw new RuntimeException(
|
||||
s"Cargo command returned a non-zero exit code: $exitCode."
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/** Checks that cargo is installed. Logs an error and returns false if not. */
|
||||
def cargoOk(log: ManagedLogger): Boolean = {
|
||||
try s"$cargoCmd version".!! catch {
|
||||
try s"$cargoCmd version".!!
|
||||
catch {
|
||||
case _: RuntimeException =>
|
||||
log.error(s"The command `cargo` isn't on path. Did you install cargo?")
|
||||
return false
|
||||
|
21
project/LauncherShimsForTest.scala
Normal file
21
project/LauncherShimsForTest.scala
Normal file
@ -0,0 +1,21 @@
|
||||
import sbt.Keys._
|
||||
import sbt._
|
||||
|
||||
object LauncherShimsForTest {
|
||||
|
||||
/**
|
||||
* Creates a task that compiles the launcher shims which are used for some of
|
||||
* the launcher tests.
|
||||
*
|
||||
* @param rustcVersion Rust version that should be used
|
||||
*/
|
||||
def prepare(rustcVersion: String): Def.Initialize[Task[Unit]] =
|
||||
Def.task {
|
||||
val log = state.value.log
|
||||
Cargo.run(
|
||||
"build -p launcher-shims",
|
||||
rustVersion = rustcVersion,
|
||||
log = log
|
||||
)
|
||||
}
|
||||
}
|
@ -159,12 +159,17 @@ object NativeImage {
|
||||
private def isLinux: Boolean =
|
||||
sys.props("os.name").toLowerCase().contains("linux")
|
||||
|
||||
private def artifactFile(name: String): File =
|
||||
/**
|
||||
* [[File]] representing the artifact called `name` built with the Native
|
||||
* Image.
|
||||
*/
|
||||
def artifactFile(name: String): File =
|
||||
if (isWindows) file(name + ".exe")
|
||||
else file(name)
|
||||
|
||||
private val muslBundleUrl =
|
||||
"https://github.com/gradinac/musl-bundle-example/releases/download/v1.0/musl.tar.gz"
|
||||
"https://github.com/gradinac/musl-bundle-example/releases/download/" +
|
||||
"v1.0/musl.tar.gz"
|
||||
|
||||
/**
|
||||
* Ensures that the `musl` bundle is installed.
|
||||
|
Loading…
Reference in New Issue
Block a user