Implement Launcher Self-Update (#1125)

This commit is contained in:
Radosław Waśko 2020-09-09 15:37:26 +02:00 committed by GitHub
parent 6301542546
commit 044a0fa664
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
77 changed files with 2831 additions and 900 deletions

View File

@ -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

View File

@ -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",

View File

@ -1061,6 +1061,9 @@ lazy val launcher = project
"enso"
)
)
.dependsOn(
LauncherShimsForTest.prepare(rustcVersion = rustVersion)
)
.value
)
.settings(licenseSettings)

View File

@ -0,0 +1,6 @@
minimum-version-for-upgrade: 0.1.0
files-to-copy:
- NOTICE
- README.md
directories-to-copy:
- components-licences

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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
}
}

View File

@ -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)
}
}

View File

@ -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(".")
/**

View File

@ -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)

View File

@ -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
}

View File

@ -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
}
}

View File

@ -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()
}

View File

@ -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())
}
}

View File

@ -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 =>

View File

@ -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`."
}
/**

View File

@ -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
)
}

View File

@ -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 {

View File

@ -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]
}

View File

@ -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.
)
)

View File

@ -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"
}
}

View File

@ -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)
}

View File

@ -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")
}

View File

@ -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]
}

View File

@ -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"
}

View File

@ -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]]
}

View File

@ -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]
}

View File

@ -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."
)
)
}
}
}
}

View File

@ -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)
/**

View File

@ -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
)
)
}
}

View File

@ -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
}

View File

@ -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
}
}

View File

@ -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 {
/**

View File

@ -1,4 +1,4 @@
package org.enso.launcher.releases
package org.enso.launcher.releases.runtime
import java.nio.file.Path

View File

@ -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)

View File

@ -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()
}
}
}

View File

@ -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
}

View File

@ -0,0 +1,3 @@
minimum-version-for-upgrade: 0.0.0
files-to-copy: []
directories-to-copy: []

View File

@ -0,0 +1,5 @@
minimum-version-for-upgrade: 0.0.0
files-to-copy:
- README.md
directories-to-copy:
- components-licences

View File

@ -0,0 +1,3 @@
minimum-version-for-upgrade: 0.0.1
files-to-copy: []
directories-to-copy: []

View File

@ -0,0 +1,3 @@
minimum-version-for-upgrade: 0.0.2
files-to-copy: []
directories-to-copy: []

View File

@ -0,0 +1,3 @@
minimum-version-for-upgrade: 0.0.2
files-to-copy: []
directories-to-copy: []

View File

@ -0,0 +1,3 @@
minimum-version-for-upgrade: 0.0.2
files-to-copy: []
directories-to-copy: []

View File

@ -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
}
}

View File

@ -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

View File

@ -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

View File

@ -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),

View File

@ -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)
}
}
}

View 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]

View 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")
}

View 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")
}

View 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")
}

View 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")
}

View 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")
}

View 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(())
}

View File

@ -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}"
}

View File

@ -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>",

View File

@ -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

View 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
)
}
}

View File

@ -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.