mirror of
https://github.com/enso-org/enso.git
synced 2024-10-26 13:14:43 +03:00
Enso Version Management in the Launcher (#1059)
- Adds support for downloading engine and runtime versions in the launcher. - Adds functionality to install, list and uninstall engine components.
This commit is contained in:
parent
65dec91bc0
commit
11868cb528
6
.gitignore
vendored
6
.gitignore
vendored
@ -94,3 +94,9 @@ bench-report.xml
|
||||
|
||||
.editorconfig
|
||||
.bloop/
|
||||
|
||||
#################
|
||||
## Build Cache ##
|
||||
#################
|
||||
|
||||
build-cache/
|
||||
|
41
build.sbt
41
build.sbt
@ -6,7 +6,7 @@ import com.typesafe.sbt.SbtLicenseReport.autoImportImpl.{
|
||||
}
|
||||
import org.enso.build.BenchTasks._
|
||||
import org.enso.build.WithDebugCommand
|
||||
import sbt.Keys.scalacOptions
|
||||
import sbt.Keys.{libraryDependencies, scalacOptions}
|
||||
import sbt.addCompilerPlugin
|
||||
import sbtassembly.AssemblyPlugin.defaultUniversalScript
|
||||
import sbtcrossproject.CrossPlugin.autoImport.{crossProject, CrossType}
|
||||
@ -305,7 +305,9 @@ val zio = Seq(
|
||||
|
||||
// === Other ==================================================================
|
||||
|
||||
val apacheHttpClientVersion = "4.5.12"
|
||||
val bcpkixJdk15Version = "1.65"
|
||||
val bumpVersion = "0.1.3"
|
||||
val declineVersion = "1.2.0"
|
||||
val directoryWatcherVersion = "0.9.10"
|
||||
val flatbuffersVersion = "1.12.0"
|
||||
@ -797,7 +799,7 @@ lazy val ast = (project in file("lib/scala/ast"))
|
||||
.settings(
|
||||
version := ensoVersion,
|
||||
GenerateAST.rustVersion := rustVersion,
|
||||
Compile / sourceGenerators += GenerateAST.task,
|
||||
Compile / sourceGenerators += GenerateAST.task
|
||||
)
|
||||
|
||||
lazy val runtime = (project in file("engine/runtime"))
|
||||
@ -974,7 +976,7 @@ lazy val runner = project
|
||||
)
|
||||
.settings(
|
||||
buildNativeImage := NativeImage
|
||||
.buildNativeImage(staticOnLinux = false)
|
||||
.buildNativeImage("enso", staticOnLinux = false)
|
||||
.value
|
||||
)
|
||||
.settings(
|
||||
@ -992,13 +994,28 @@ lazy val launcher = project
|
||||
.in(file("engine/launcher"))
|
||||
.configs(Test)
|
||||
.settings(
|
||||
resolvers += Resolver.bintrayRepo("gn0s1s", "releases"),
|
||||
libraryDependencies ++= Seq(
|
||||
"org.scalatest" %% "scalatest" % scalatestVersion % Test,
|
||||
"org.typelevel" %% "cats-core" % catsVersion
|
||||
"org.scalatest" %% "scalatest" % scalatestVersion % Test,
|
||||
"org.typelevel" %% "cats-core" % catsVersion,
|
||||
"nl.gn0s1s" %% "bump" % bumpVersion,
|
||||
"org.apache.commons" % "commons-compress" % commonsCompressVersion,
|
||||
"org.apache.httpcomponents" % "httpclient" % apacheHttpClientVersion
|
||||
)
|
||||
)
|
||||
.settings(
|
||||
buildNativeImage := NativeImage.buildNativeImage(staticOnLinux = true).value
|
||||
buildNativeImage := NativeImage
|
||||
.buildNativeImage(
|
||||
"enso",
|
||||
staticOnLinux = true,
|
||||
Seq(
|
||||
"--enable-all-security-services", // Note [HTTPS in the Launcher]
|
||||
"-Dorg.apache.commons.logging.Log=org.apache.commons.logging.impl.NoOpLog"
|
||||
)
|
||||
)
|
||||
.value,
|
||||
test in assembly := {},
|
||||
assemblyOutputPath in assembly := file("launcher.jar")
|
||||
)
|
||||
.settings(
|
||||
(Test / test) := (Test / test)
|
||||
@ -1014,3 +1031,15 @@ lazy val launcher = project
|
||||
.dependsOn(cli)
|
||||
.dependsOn(`version-output`)
|
||||
.dependsOn(pkg)
|
||||
|
||||
/* Note [HTTPS in the Launcher]
|
||||
* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
* The launcher uses Apache HttpClient for making web requests. It does not use
|
||||
* Java's stdlib implementation, because there is a bug (not fixed in JDK 11)
|
||||
* (https://bugs.openjdk.java.net/browse/JDK-8231449) in its HTTPS handling that
|
||||
* causes long running requests to freeze forever. However, Apache HttpClient
|
||||
* still needs the stdlib's SSL implementation and it is not included in the
|
||||
* Native Images by default (because of its size). The
|
||||
* `--enable-all-security-services` flag is used to ensure it is available in
|
||||
* the built executable.
|
||||
*/
|
||||
|
@ -86,6 +86,120 @@ Copyright 2002-2017 The Apache Software Foundation
|
||||
This product includes software developed at
|
||||
The Apache Software Foundation (http://www.apache.org/).
|
||||
|
||||
-------------------------
|
||||
|
||||
Apache Commons Codec, licensed under Apache License Version 2.0
|
||||
(see: components-licences/LICENSE-APACHE) is distributed with the launcher.
|
||||
It requires the following notice:
|
||||
Apache Commons Codec
|
||||
Copyright 2002-2017 The Apache Software Foundation
|
||||
|
||||
This product includes software developed at
|
||||
The Apache Software Foundation (http://www.apache.org/).
|
||||
|
||||
src/test/org/apache/commons/codec/language/DoubleMetaphoneTest.java
|
||||
contains test data from http://aspell.net/test/orig/batch0.tab.
|
||||
Copyright (C) 2002 Kevin Atkinson (kevina@gnu.org)
|
||||
|
||||
===============================================================================
|
||||
|
||||
The content of package org.apache.commons.codec.language.bm has been translated
|
||||
from the original php source code available at http://stevemorse.org/phoneticinfo.htm
|
||||
with permission from the original authors.
|
||||
Original source copyright:
|
||||
Copyright (c) 2008 Alexander Beider & Stephen P. Morse.
|
||||
|
||||
-----------------------
|
||||
|
||||
Apache Commons Compress, licensed under Apache License Version 2.0
|
||||
(see: components-licences/LICENSE-APACHE) is distributed with the launcher.
|
||||
It requires the following notice:
|
||||
Apache Commons Compress
|
||||
Copyright 2002-2020 The Apache Software Foundation
|
||||
|
||||
This product includes software developed at
|
||||
The Apache Software Foundation (https://www.apache.org/).
|
||||
|
||||
---
|
||||
|
||||
The files in the package org.apache.commons.compress.archivers.sevenz
|
||||
were derived from the LZMA SDK, version 9.20 (C/ and CPP/7zip/),
|
||||
which has been placed in the public domain:
|
||||
|
||||
"LZMA SDK is placed in the public domain." (http://www.7-zip.org/sdk.html)
|
||||
|
||||
---
|
||||
|
||||
The test file lbzip2_32767.bz2 has been copied from libbzip2's source
|
||||
repository:
|
||||
|
||||
This program, "bzip2", the associated library "libbzip2", and all
|
||||
documentation, are copyright (C) 1996-2019 Julian R Seward. All
|
||||
rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions
|
||||
are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
|
||||
2. The origin of this software must not be misrepresented; you must
|
||||
not claim that you wrote the original software. If you use this
|
||||
software in a product, an acknowledgment in the product
|
||||
documentation would be appreciated but is not required.
|
||||
|
||||
3. Altered source versions must be plainly marked as such, and must
|
||||
not be misrepresented as being the original software.
|
||||
|
||||
4. The name of the author may not be used to endorse or promote
|
||||
products derived from this software without specific prior written
|
||||
permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS
|
||||
OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
|
||||
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
|
||||
GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
|
||||
WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
Julian Seward, jseward@acm.org
|
||||
|
||||
-----------------------
|
||||
|
||||
Apache HttpClient, licensed under Apache License Version 2.0
|
||||
(see: components-licences/LICENSE-APACHE) is distributed with the launcher.
|
||||
It requires the following notice:
|
||||
|
||||
Apache HttpClient
|
||||
Copyright 1999-2020 The Apache Software Foundation
|
||||
|
||||
This product includes software developed at
|
||||
The Apache Software Foundation (http://www.apache.org/).
|
||||
|
||||
|
||||
-----------------------
|
||||
|
||||
Apache HttpCore, licensed under Apache License Version 2.0
|
||||
(see: components-licences/LICENSE-APACHE) is distributed with the launcher.
|
||||
It requires the following notice:
|
||||
|
||||
Apache HttpCore
|
||||
Copyright 2005-2020 The Apache Software Foundation
|
||||
|
||||
This product includes software developed at
|
||||
The Apache Software Foundation (http://www.apache.org/).
|
||||
|
||||
|
||||
-----------------------
|
||||
|
||||
|
||||
|
||||
=======================
|
||||
|
||||
As the launcher is written in Scala and built with Graal Native Image, its
|
||||
@ -128,4 +242,50 @@ the launcher.
|
||||
================
|
||||
|
||||
'snakeyaml', licensed under the Apache License, Version 2.0, is distributed with
|
||||
the launcher.
|
||||
the launcher.
|
||||
|
||||
================
|
||||
|
||||
'bump', licensed under the MIT License, is distributed with the launcher. It
|
||||
requires the following notice:
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2018-2020 Philippus Baalman
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
================
|
||||
|
||||
The launcher uses the 'Scala parser combinators' library, licensed under the
|
||||
Apache License, Version 2.0. It requires the following notice:
|
||||
|
||||
Scala parser combinators
|
||||
Copyright (c) 2002-2020 EPFL
|
||||
Copyright (c) 2011-2020 Lightbend, Inc.
|
||||
|
||||
Scala includes software developed at
|
||||
LAMP/EPFL (https://lamp.epfl.ch/) and
|
||||
Lightbend, Inc. (https://www.lightbend.com/).
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License").
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
================
|
||||
|
||||
The version of this launcher built for the Linux platform uses `zlib` created by
|
||||
Jean-loup Gailly and Mark Adler.
|
||||
|
||||
================
|
||||
|
||||
The version of this launcher built for the Linux platform uses the `musl` libc
|
||||
licensed under the MIT license. Its copyright notice is included in
|
||||
`components-licences/COPYRIGHT-MUSL`.
|
||||
|
190
distribution/launcher/components-licences/COPYRIGHT-MUSL
Normal file
190
distribution/launcher/components-licences/COPYRIGHT-MUSL
Normal file
@ -0,0 +1,190 @@
|
||||
musl as a whole is licensed under the following standard MIT license:
|
||||
|
||||
----------------------------------------------------------------------
|
||||
Copyright © 2005-2020 Rich Felker, et al.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
"Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
----------------------------------------------------------------------
|
||||
|
||||
Authors/contributors include:
|
||||
|
||||
A. Wilcox
|
||||
Ada Worcester
|
||||
Alex Dowad
|
||||
Alex Suykov
|
||||
Alexander Monakov
|
||||
Andre McCurdy
|
||||
Andrew Kelley
|
||||
Anthony G. Basile
|
||||
Aric Belsito
|
||||
Arvid Picciani
|
||||
Bartosz Brachaczek
|
||||
Benjamin Peterson
|
||||
Bobby Bingham
|
||||
Boris Brezillon
|
||||
Brent Cook
|
||||
Chris Spiegel
|
||||
Clément Vasseur
|
||||
Daniel Micay
|
||||
Daniel Sabogal
|
||||
Daurnimator
|
||||
David Carlier
|
||||
David Edelsohn
|
||||
Denys Vlasenko
|
||||
Dmitry Ivanov
|
||||
Dmitry V. Levin
|
||||
Drew DeVault
|
||||
Emil Renner Berthing
|
||||
Fangrui Song
|
||||
Felix Fietkau
|
||||
Felix Janda
|
||||
Gianluca Anzolin
|
||||
Hauke Mehrtens
|
||||
He X
|
||||
Hiltjo Posthuma
|
||||
Isaac Dunham
|
||||
Jaydeep Patil
|
||||
Jens Gustedt
|
||||
Jeremy Huntwork
|
||||
Jo-Philipp Wich
|
||||
Joakim Sindholt
|
||||
John Spencer
|
||||
Julien Ramseier
|
||||
Justin Cormack
|
||||
Kaarle Ritvanen
|
||||
Khem Raj
|
||||
Kylie McClain
|
||||
Leah Neukirchen
|
||||
Luca Barbato
|
||||
Luka Perkov
|
||||
M Farkas-Dyck (Strake)
|
||||
Mahesh Bodapati
|
||||
Markus Wichmann
|
||||
Masanori Ogino
|
||||
Michael Clark
|
||||
Michael Forney
|
||||
Mikhail Kremnyov
|
||||
Natanael Copa
|
||||
Nicholas J. Kain
|
||||
orc
|
||||
Pascal Cuoq
|
||||
Patrick Oppenlander
|
||||
Petr Hosek
|
||||
Petr Skocik
|
||||
Pierre Carrier
|
||||
Reini Urban
|
||||
Rich Felker
|
||||
Richard Pennington
|
||||
Ryan Fairfax
|
||||
Samuel Holland
|
||||
Segev Finer
|
||||
Shiz
|
||||
sin
|
||||
Solar Designer
|
||||
Stefan Kristiansson
|
||||
Stefan O'Rear
|
||||
Szabolcs Nagy
|
||||
Timo Teräs
|
||||
Trutz Behn
|
||||
Valentin Ochs
|
||||
Will Dietz
|
||||
William Haddon
|
||||
William Pitcock
|
||||
|
||||
Portions of this software are derived from third-party works licensed
|
||||
under terms compatible with the above MIT license:
|
||||
|
||||
The TRE regular expression implementation (src/regex/reg* and
|
||||
src/regex/tre*) is Copyright © 2001-2008 Ville Laurikari and licensed
|
||||
under a 2-clause BSD license (license text in the source files). The
|
||||
included version has been heavily modified by Rich Felker in 2012, in
|
||||
the interests of size, simplicity, and namespace cleanliness.
|
||||
|
||||
Much of the math library code (src/math/* and src/complex/*) is
|
||||
Copyright © 1993,2004 Sun Microsystems or
|
||||
Copyright © 2003-2011 David Schultz or
|
||||
Copyright © 2003-2009 Steven G. Kargl or
|
||||
Copyright © 2003-2009 Bruce D. Evans or
|
||||
Copyright © 2008 Stephen L. Moshier or
|
||||
Copyright © 2017-2018 Arm Limited
|
||||
and labelled as such in comments in the individual source files. All
|
||||
have been licensed under extremely permissive terms.
|
||||
|
||||
The ARM memcpy code (src/string/arm/memcpy_el.S) is Copyright © 2008
|
||||
The Android Open Source Project and is licensed under a two-clause BSD
|
||||
license. It was taken from Bionic libc, used on Android.
|
||||
|
||||
The implementation of DES for crypt (src/crypt/crypt_des.c) is
|
||||
Copyright © 1994 David Burren. It is licensed under a BSD license.
|
||||
|
||||
The implementation of blowfish crypt (src/crypt/crypt_blowfish.c) was
|
||||
originally written by Solar Designer and placed into the public
|
||||
domain. The code also comes with a fallback permissive license for use
|
||||
in jurisdictions that may not recognize the public domain.
|
||||
|
||||
The smoothsort implementation (src/stdlib/qsort.c) is Copyright © 2011
|
||||
Valentin Ochs and is licensed under an MIT-style license.
|
||||
|
||||
The x86_64 port was written by Nicholas J. Kain and is licensed under
|
||||
the standard MIT terms.
|
||||
|
||||
The mips and microblaze ports were originally written by Richard
|
||||
Pennington for use in the ellcc project. The original code was adapted
|
||||
by Rich Felker for build system and code conventions during upstream
|
||||
integration. It is licensed under the standard MIT terms.
|
||||
|
||||
The mips64 port was contributed by Imagination Technologies and is
|
||||
licensed under the standard MIT terms.
|
||||
|
||||
The powerpc port was also originally written by Richard Pennington,
|
||||
and later supplemented and integrated by John Spencer. It is licensed
|
||||
under the standard MIT terms.
|
||||
|
||||
All other files which have no copyright comments are original works
|
||||
produced specifically for use as part of this library, written either
|
||||
by Rich Felker, the main author of the library, or by one or more
|
||||
contibutors listed above. Details on authorship of individual files
|
||||
can be found in the git version control history of the project. The
|
||||
omission of copyright and license comments in each file is in the
|
||||
interest of source tree size.
|
||||
|
||||
In addition, permission is hereby granted for all public header files
|
||||
(include/* and arch/*/bits/*) and crt files intended to be linked into
|
||||
applications (crt/*, ldso/dlstart.c, and arch/*/crt_arch.h) to omit
|
||||
the copyright notice and permission notice otherwise required by the
|
||||
license, and to use these files without any requirement of
|
||||
attribution. These files include substantial contributions from:
|
||||
|
||||
Bobby Bingham
|
||||
John Spencer
|
||||
Nicholas J. Kain
|
||||
Rich Felker
|
||||
Richard Pennington
|
||||
Stefan Kristiansson
|
||||
Szabolcs Nagy
|
||||
|
||||
all of whom have explicitly granted such permission.
|
||||
|
||||
This file previously contained text expressing a belief that most of
|
||||
the files covered by the above exception were sufficiently trivial not
|
||||
to be subject to copyright, resulting in confusion over whether it
|
||||
negated the permissions granted in the license. In the spirit of
|
||||
permissive licensing, and of not having licensing issues being an
|
||||
obstacle to adoption, that text has been removed.
|
@ -124,6 +124,16 @@ compiler that they relate to.
|
||||
|
||||
### System Requirements
|
||||
|
||||
The following operating systems are supported for developing Enso:
|
||||
|
||||
- Windows 10
|
||||
- macOS 10.14 and above
|
||||
- Linux 4.4 and above
|
||||
|
||||
Currently only the x86_64 (amd64) architecture is supported. You may be able to
|
||||
develop Enso on other systems, but issues arising from unsupported
|
||||
configurations will not be fixed by the core team.
|
||||
|
||||
In order to build and run Enso you will need the following tools:
|
||||
|
||||
- [sbt](https://www.scala-sbt.org/) with the same version as specified in
|
||||
@ -137,6 +147,8 @@ In order to build and run Enso you will need the following tools:
|
||||
- [Cargo](https://doc.rust-lang.org/cargo/getting-started/installation.html),
|
||||
the rust build tool.
|
||||
- [Rustup](https://rustup.rs), the rust toolchain management utility.
|
||||
- On MacOS and Linux, the `tar` command is required for running some tests. It
|
||||
should be installed by default on most distributions.
|
||||
|
||||
Managing multiple JVM installations can be a pain, so some of the team use
|
||||
[Jenv](http://www.jenv.be/): A useful tool for managing multiple JVMs.
|
||||
@ -172,8 +184,8 @@ git clone git@github.com:enso-org/enso.git
|
||||
|
||||
### Getting Set Up (Rust)
|
||||
|
||||
The SBT project requires a specific nightly rust toolchain. To get it set up,
|
||||
you will need to install [rustup](https://rustup.rs/) and then run the following
|
||||
The SBT project requires a specific nightly rust toolchain. To get it set up,
|
||||
you will need to install [rustup](https://rustup.rs/) and then run the following
|
||||
commands:
|
||||
|
||||
```bash
|
||||
@ -400,9 +412,9 @@ need to follow these steps:
|
||||
placeholders that we don't use.
|
||||
6. Alternatively, certain tasks, such as `run`, `benchOnly` and `testOnly` can
|
||||
be used through the `withDebug` SBT command. For this to work, your remote
|
||||
configuration must specify the host of `localhost` and the port `5005`.
|
||||
The command syntax is `withDebug --debugger TASK_NAME -- TASK_PARAMETERS`,
|
||||
e.g. `withDebug --debugger testOnly -- *AtomConstructors*`.
|
||||
configuration must specify the host of `localhost` and the port `5005`. The
|
||||
command syntax is `withDebug --debugger TASK_NAME -- TASK_PARAMETERS`, e.g.
|
||||
`withDebug --debugger testOnly -- *AtomConstructors*`.
|
||||
7. Now, when you want to debug something, you can place a breakpoint as usual in
|
||||
IntelliJ, and then execute your remote debugging configuration. Now, in the
|
||||
SBT shell, run a command to execute the code you want to debug (e.g.
|
||||
|
@ -110,7 +110,7 @@ For example:
|
||||
```yaml
|
||||
minimum-launcher-version: 0.0.1
|
||||
graal-vm-version: 20.1.0
|
||||
graal-java-version: java11
|
||||
graal-java-version: 11
|
||||
```
|
||||
|
||||
The `minimum-launcher-version` should be updated whenever a new version of Enso
|
||||
|
@ -15,3 +15,5 @@ up as follows:
|
||||
- [**sbt:**](sbt.md) The build tools that are used for building the project.
|
||||
- [**Java 11:**](java-11.md) Description of changes related to the Java 11
|
||||
migration.
|
||||
- [**Native Image:**](native-image.md) Description of the Native Image build
|
||||
used for building the launcher native binary.
|
||||
|
116
docs/infrastructure/native-image.md
Normal file
116
docs/infrastructure/native-image.md
Normal file
@ -0,0 +1,116 @@
|
||||
---
|
||||
layout: developer-doc
|
||||
title: Native Image
|
||||
category: infrastructure
|
||||
tags: [infrastructure, build, native, native-image]
|
||||
order: 3
|
||||
---
|
||||
|
||||
# Native Image
|
||||
|
||||
[`NativeImage`](../../project/NativeImage.scala) defines a task that is used for
|
||||
compiling a project into a native binary using Graal's Native Image. It compiles
|
||||
the project and runs the Native Image tool which builds the image. Currently,
|
||||
Native Image is used for building the Launcher.
|
||||
|
||||
<!-- MarkdownTOC levels="2,3" autolink="true" -->
|
||||
|
||||
- [Requirements](#requirements)
|
||||
- [Native Image Component](#native-image-component)
|
||||
- [Additional Linux Dependencies](#additional-linux-dependencies)
|
||||
- [Static Builds](#static-builds)
|
||||
- [No Cross-Compilation](#no-cross-compilation)
|
||||
|
||||
<!-- /MarkdownTOC -->
|
||||
|
||||
## Requirements
|
||||
|
||||
### Native Image Component
|
||||
|
||||
The Native Image component has to be installed within the used GraalVM
|
||||
distribution. It can be installed by running `gu install native-image`.
|
||||
|
||||
### Additional Linux Dependencies
|
||||
|
||||
To be able to [link statically](#static-builds) on Linux, we need to link
|
||||
against a `libc` implementation. The default `glibc` contains
|
||||
[a bug](https://sourceware.org/bugzilla/show_bug.cgi?id=10652) that would cause
|
||||
crashes when downloading files form the internet, which is a crucial Launcher
|
||||
functionality. Instead, [`musl`](https://musl.libc.org/) implementation is
|
||||
suggested by Graal as an alternative. The sbt task automatically downloads a
|
||||
bundle containing all requirements for a static build with `musl`. It only
|
||||
requires a `tar` command to be available to extract the bundle.
|
||||
|
||||
Currently, to enable compiling with `musl`, `-H:UseMuslC=/path/to/musl/bundle`
|
||||
option is added to the `native-image` command. In the future, the command may
|
||||
use a different option for enabling `musl` or even enable it by default, so the
|
||||
Native Image task may need updating with a newer Graal release. More information
|
||||
may be found in
|
||||
[the Native Image documentation](https://github.com/oracle/graal/blob/master/substratevm/STATIC-IMAGES.md).
|
||||
|
||||
## Static Builds
|
||||
|
||||
The task is parametrized with `staticOnLinux` parameter which if set to `true`,
|
||||
will statically link the built binary, to ensure portability between Linux
|
||||
distributions. For Windows and MacOS, the binaries should generally be portable,
|
||||
as described in [Launcher Portability](../distribution/launcher.md#portability).
|
||||
|
||||
## No Cross-Compilation
|
||||
|
||||
As Native Image does not support cross-compilation, the native binaries can only
|
||||
be built for the platform and architecture that the build is running on.
|
||||
|
||||
## Configuration
|
||||
|
||||
As the Native Image builds a native binary, certain capabilities, like
|
||||
[reflection](https://github.com/oracle/graal/blob/master/substratevm/REFLECTION.md),
|
||||
may be limited. The build system tries to automatically detect some reflective
|
||||
accesses, but it cannot detect everything. It is possible for the built binary
|
||||
to fail with the following error:
|
||||
|
||||
```
|
||||
java.lang.InstantiationException: Type `XYZ` can not be instantiated reflectively as it does not have a no-parameter constructor or the no-parameter constructor has not been added explicitly to the native image.`
|
||||
```
|
||||
|
||||
To avoid such issues, additional configuration has to be added to the Native
|
||||
Image build so that it can include the missing constructors.
|
||||
|
||||
This can be done manually by creating a file `reflect-config.json`. The build
|
||||
task looks for the configuration files in a directory called
|
||||
`native-image-config` inside the root of the compiled sub-project.
|
||||
|
||||
Creating the configuration manually may be tedious and error-prone, so GraalVM
|
||||
includes
|
||||
[a tool for assisted configuration](https://github.com/oracle/graal/blob/master/substratevm/CONFIGURE.md).
|
||||
The link describes in detail how the tool can be used. The gist is, the JVM
|
||||
version of the application can be run with a special agentlib in order to trace
|
||||
reflective accesses and save the generated configuration to the provided
|
||||
directory. To run the tool it is easiest to assemble the application into a JAR
|
||||
and run it with the following command:
|
||||
|
||||
```bash
|
||||
java \
|
||||
-agentlib:native-image-agent=config-merge-dir=/path/to/native-image-config \
|
||||
-jar /path/to/application.jar \
|
||||
<application arguments>
|
||||
```
|
||||
|
||||
For example, to update settings for the Launcher:
|
||||
|
||||
```bash
|
||||
java -agentlib:native-image-agent=config-merge-dir=engine/launcher/native-image-config -jar launcher.jar <arguments>
|
||||
```
|
||||
|
||||
The command may need to be re-run with different arguments to ensure that all
|
||||
execution paths that use reflection are covered. The configuration files between
|
||||
consecutive runs will be merged (a warning may be issued for the first run if
|
||||
the configuration files did not exist, this is not a problem).
|
||||
|
||||
It is possible that different classes are reflectively accessed on different
|
||||
platforms. In that case it may be necessary to run the agent on multiple
|
||||
platforms and merge the configs. If the conflicts were conflicting (i.e. some
|
||||
reflectively accessed classes existed only on one platform), it may be necessary
|
||||
to maintain separate configs for each platform. Currently in the Launcher this
|
||||
is not the case - the reflective accesses seem to be platform independent, as
|
||||
the launcher built with a config created on Linux runs successfully on other
|
||||
platforms.
|
@ -26,6 +26,8 @@ compilation. The build configuration is defined in
|
||||
- [Flatbuffers Generation](#flatbuffers-generation)
|
||||
- [Ensuring JARs Were Loaded](#ensuring-jars-were-loaded)
|
||||
- [Debugging Command](#debugging-command)
|
||||
- [Recompile Parser](#recompile-parser)
|
||||
- [Native Image](#native-image)
|
||||
|
||||
<!-- /MarkdownTOC -->
|
||||
|
||||
@ -189,16 +191,5 @@ This task ensures that the `syntax` project is recompiled whenever
|
||||
|
||||
## Native Image
|
||||
|
||||
[`NativeImage`](../../project/NativeImage.scala) defines a task that can compile
|
||||
a project into a native binary using Graal's Native Image. It compiles the
|
||||
project and runs the Native Image tool which builds the image. To be able to use
|
||||
it, the Native Image component has to be installed within the used GraalVM
|
||||
distribution. It can be installed by running `gu install native-image`.
|
||||
|
||||
The task is parametrized with `staticOnLinux` parameter which if set to `true`,
|
||||
will statically link the built binary, to ensure portability between Linux
|
||||
distributions. For Windows and MacOS, the binaries should generally be portable,
|
||||
as described in [Launcher Portability](../distribution/launcher.md#portability).
|
||||
|
||||
As Native Image does not support cross-compilation, the native binaries can only
|
||||
be built for the platform and architecture that the build is running on.
|
||||
[`NativeImage`](../../project/NativeImage.scala) task is described at
|
||||
[Native Image](native-image.md).
|
||||
|
@ -66,8 +66,8 @@ Vec => Vector,
|
||||
Uuid => UUID,
|
||||
```
|
||||
|
||||
*Note: It is assumed, that Enso runs on 64bit platforms. Therefore, `usize` and
|
||||
`isize` are converted to `Long`.*
|
||||
> Note: It is assumed, that Enso runs on 64bit platforms. Therefore, `usize` and
|
||||
> `isize` are converted to `Long`.
|
||||
|
||||
##### Structures With Named Fields
|
||||
|
||||
|
15
engine/launcher/native-image-config/jni-config.json
Normal file
15
engine/launcher/native-image-config/jni-config.json
Normal file
@ -0,0 +1,15 @@
|
||||
[
|
||||
{
|
||||
"name": "java.lang.ClassLoader",
|
||||
"methods": [
|
||||
{ "name": "getPlatformClassLoader", "parameterTypes": [] },
|
||||
{ "name": "loadClass", "parameterTypes": ["java.lang.String"] }
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "java.lang.ClassNotFoundException"
|
||||
},
|
||||
{
|
||||
"name": "java.lang.NoSuchMethodError"
|
||||
}
|
||||
]
|
1
engine/launcher/native-image-config/proxy-config.json
Normal file
1
engine/launcher/native-image-config/proxy-config.json
Normal file
@ -0,0 +1 @@
|
||||
[]
|
88
engine/launcher/native-image-config/reflect-config.json
Normal file
88
engine/launcher/native-image-config/reflect-config.json
Normal file
@ -0,0 +1,88 @@
|
||||
[
|
||||
{
|
||||
"name": "java.lang.String"
|
||||
},
|
||||
{
|
||||
"name": "java.lang.String[]"
|
||||
},
|
||||
{
|
||||
"name": "java.lang.invoke.VarHandle"
|
||||
},
|
||||
{
|
||||
"name": "java.sql.Date"
|
||||
},
|
||||
{
|
||||
"name": "java.sql.Timestamp"
|
||||
},
|
||||
{
|
||||
"name": "org.apache.commons.compress.archivers.zip.AsiExtraField",
|
||||
"methods": [{ "name": "<init>", "parameterTypes": [] }]
|
||||
},
|
||||
{
|
||||
"name": "org.apache.commons.compress.archivers.zip.JarMarker",
|
||||
"methods": [{ "name": "<init>", "parameterTypes": [] }]
|
||||
},
|
||||
{
|
||||
"name": "org.apache.commons.compress.archivers.zip.ResourceAlignmentExtraField",
|
||||
"methods": [{ "name": "<init>", "parameterTypes": [] }]
|
||||
},
|
||||
{
|
||||
"name": "org.apache.commons.compress.archivers.zip.UnicodeCommentExtraField",
|
||||
"methods": [{ "name": "<init>", "parameterTypes": [] }]
|
||||
},
|
||||
{
|
||||
"name": "org.apache.commons.compress.archivers.zip.UnicodePathExtraField",
|
||||
"methods": [{ "name": "<init>", "parameterTypes": [] }]
|
||||
},
|
||||
{
|
||||
"name": "org.apache.commons.compress.archivers.zip.X000A_NTFS",
|
||||
"methods": [{ "name": "<init>", "parameterTypes": [] }]
|
||||
},
|
||||
{
|
||||
"name": "org.apache.commons.compress.archivers.zip.X0014_X509Certificates",
|
||||
"methods": [{ "name": "<init>", "parameterTypes": [] }]
|
||||
},
|
||||
{
|
||||
"name": "org.apache.commons.compress.archivers.zip.X0015_CertificateIdForFile",
|
||||
"methods": [{ "name": "<init>", "parameterTypes": [] }]
|
||||
},
|
||||
{
|
||||
"name": "org.apache.commons.compress.archivers.zip.X0016_CertificateIdForCentralDirectory",
|
||||
"methods": [{ "name": "<init>", "parameterTypes": [] }]
|
||||
},
|
||||
{
|
||||
"name": "org.apache.commons.compress.archivers.zip.X0017_StrongEncryptionHeader",
|
||||
"methods": [{ "name": "<init>", "parameterTypes": [] }]
|
||||
},
|
||||
{
|
||||
"name": "org.apache.commons.compress.archivers.zip.X0019_EncryptionRecipientCertificateList",
|
||||
"methods": [{ "name": "<init>", "parameterTypes": [] }]
|
||||
},
|
||||
{
|
||||
"name": "org.apache.commons.compress.archivers.zip.X5455_ExtendedTimestamp",
|
||||
"methods": [{ "name": "<init>", "parameterTypes": [] }]
|
||||
},
|
||||
{
|
||||
"name": "org.apache.commons.compress.archivers.zip.X7875_NewUnix",
|
||||
"methods": [{ "name": "<init>", "parameterTypes": [] }]
|
||||
},
|
||||
{
|
||||
"name": "org.apache.commons.compress.archivers.zip.Zip64ExtendedInformationExtraField",
|
||||
"methods": [{ "name": "<init>", "parameterTypes": [] }]
|
||||
},
|
||||
{
|
||||
"name": "org.apache.commons.logging.LogFactory"
|
||||
},
|
||||
{
|
||||
"name": "org.apache.commons.logging.impl.LogFactoryImpl",
|
||||
"methods": [{ "name": "<init>", "parameterTypes": [] }]
|
||||
},
|
||||
{
|
||||
"name": "org.apache.commons.logging.impl.NoOpLog",
|
||||
"methods": [{ "name": "<init>", "parameterTypes": ["java.lang.String"] }]
|
||||
},
|
||||
{
|
||||
"name": "org.apache.commons.logging.impl.WeakHashtable",
|
||||
"methods": [{ "name": "<init>", "parameterTypes": [] }]
|
||||
}
|
||||
]
|
8
engine/launcher/native-image-config/resource-config.json
Normal file
8
engine/launcher/native-image-config/resource-config.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"resources": [
|
||||
{ "pattern": "\\QMain.enso\\E" },
|
||||
{ "pattern": "\\Qmozilla/public-suffix-list.txt\\E" },
|
||||
{ "pattern": "\\Qorg/apache/http/client/version.properties\\E" }
|
||||
],
|
||||
"bundles": []
|
||||
}
|
@ -1,6 +1,159 @@
|
||||
package org.enso.launcher
|
||||
|
||||
import java.io.File
|
||||
import java.nio.file.Path
|
||||
|
||||
import scala.util.Try
|
||||
|
||||
/**
|
||||
* The default [[internal.Environment]] implementation.
|
||||
* Gathers some helper methods querying the system environment.
|
||||
*
|
||||
* The default implementations should be used most of the time, but it is a
|
||||
* trait so that the functions can be overridden in tests.
|
||||
*/
|
||||
object Environment extends internal.Environment
|
||||
trait Environment {
|
||||
|
||||
/**
|
||||
* Returns a list of system-dependent plugin extensions.
|
||||
*
|
||||
* By default, on Unix plugins should have no extensions. On Windows, `.exe`
|
||||
* `.bat` and `.cmd` are supported.
|
||||
*/
|
||||
def getPluginExtensions: Seq[String] =
|
||||
if (OS.isWindows)
|
||||
Seq(".exe", ".bat", ".cmd")
|
||||
else Seq()
|
||||
|
||||
/**
|
||||
* Returns a list of directories that can be ignored when traversing the
|
||||
* system PATH looking for plugins.
|
||||
*
|
||||
* These could be system directories that should not contain plguins anyway,
|
||||
* but traversing them would greatly slow down plugin discovery.
|
||||
*/
|
||||
def getIgnoredPathDirectories: Seq[Path] =
|
||||
if (OS.isWindows) Seq(Path.of("C:\\Windows")) else Seq()
|
||||
|
||||
/**
|
||||
* Queries the system environment for the given variable that should
|
||||
* represent a valid filesystem path.
|
||||
*
|
||||
* If it is not defined or is not a valid path, returns None.
|
||||
*/
|
||||
def getEnvPath(key: String): Option[Path] = {
|
||||
def parsePathWithWarning(str: String): Option[Path] = {
|
||||
val result = safeParsePath(str)
|
||||
if (result.isEmpty) {
|
||||
Logger.warn(
|
||||
s"System variable `$key` was set (to value `$str`), but it did not " +
|
||||
s"represent a valid path, so it has been ignored."
|
||||
)
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
getEnvVar(key).flatMap(parsePathWithWarning)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the system PATH, if available.
|
||||
*/
|
||||
def getSystemPath: Seq[Path] =
|
||||
getEnvVar("PATH")
|
||||
.map(_.split(File.pathSeparatorChar).toSeq.flatMap(safeParsePath))
|
||||
.getOrElse(Seq())
|
||||
|
||||
/**
|
||||
* Returns the location of the HOME directory on Unix systems.
|
||||
*
|
||||
* Should not be called on Windows, as the concept of HOME should be handled
|
||||
* differently there.
|
||||
*/
|
||||
def getHome: Path = {
|
||||
if (OS.isWindows)
|
||||
throw new IllegalStateException(
|
||||
"fatal error: HOME should not be queried on Windows"
|
||||
)
|
||||
else {
|
||||
getEnvVar("HOME").flatMap(safeParsePath) match {
|
||||
case Some(path) => path
|
||||
case None =>
|
||||
throw new RuntimeException(
|
||||
"fatal error: HOME environment variable is not defined."
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the location of the local application data directory
|
||||
* (`%LocalAppData%`) on Windows.
|
||||
*
|
||||
* Should not be called on platforms other than Windows, as this concept is
|
||||
* defined in different ways there.
|
||||
*/
|
||||
def getLocalAppData: Path = {
|
||||
if (!OS.isWindows)
|
||||
throw new IllegalStateException(
|
||||
"fatal error: LocalAppData should be queried only on Windows"
|
||||
)
|
||||
else {
|
||||
getEnvVar("LocalAppData").flatMap(safeParsePath) match {
|
||||
case Some(path) => path
|
||||
case None =>
|
||||
throw new RuntimeException(
|
||||
"fatal error: %LocalAppData% environment variable is not defined."
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Queries the system environment for the given variable.
|
||||
*
|
||||
* If it is not defined or empty, returns None.
|
||||
*/
|
||||
def getEnvVar(key: String): Option[String] = {
|
||||
val value = System.getenv(key)
|
||||
if (value == null || value == "") None
|
||||
else Some(value)
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to parse a path string and returns Some(path) on success.
|
||||
*
|
||||
* We prefer silent failures here (returning None and skipping that entry),
|
||||
* as we don't want to fail the whole command if the PATH contains some
|
||||
* unparseable entries.
|
||||
*/
|
||||
private def safeParsePath(str: String): Option[Path] =
|
||||
Try(Path.of(str)).toOption
|
||||
|
||||
/**
|
||||
* Returns the path to the running program.
|
||||
*
|
||||
* It is intended for usage in native binary builds, where it returns the
|
||||
* path to the binary executable that is running. When running on the JVM,
|
||||
* returns a path to the root of the classpath for the `org.enso.launcher`
|
||||
* package or a built JAR.
|
||||
*/
|
||||
def getPathToRunningExecutable: Path = {
|
||||
try {
|
||||
val codeSource =
|
||||
this.getClass.getProtectionDomain.getCodeSource
|
||||
Path.of(codeSource.getLocation.toURI).toAbsolutePath
|
||||
} catch {
|
||||
case e: Exception =>
|
||||
throw new IllegalStateException(
|
||||
"Cannot locate the path of the launched executable",
|
||||
e
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The default [[Environment]] implementation.
|
||||
*/
|
||||
object Environment extends Environment
|
||||
|
@ -1,15 +1,15 @@
|
||||
package org.enso.launcher
|
||||
|
||||
import java.io.PrintWriter
|
||||
import java.nio.file.attribute.{PosixFilePermission, PosixFilePermissions}
|
||||
import java.nio.file.{Files, Path, StandardCopyOption}
|
||||
import java.util
|
||||
|
||||
import org.apache.commons.io.FileUtils
|
||||
import java.nio.file.{Files, Path}
|
||||
|
||||
import org.enso.launcher.internal.OS
|
||||
|
||||
import scala.collection.Factory
|
||||
import scala.jdk.StreamConverters._
|
||||
import sys.process._
|
||||
import scala.util.Using
|
||||
|
||||
/**
|
||||
* Gathers some helper methods that are used for interaction with the
|
||||
@ -25,19 +25,14 @@ object FileSystem {
|
||||
*/
|
||||
def listDirectory(dir: Path): Seq[Path] =
|
||||
if (!Files.exists(dir)) Seq()
|
||||
else Files.list(dir).toScala(Factory.arrayFactory).toSeq
|
||||
else Using(Files.list(dir))(_.toScala(Factory.arrayFactory).toSeq).get
|
||||
|
||||
/**
|
||||
* Writes a String to a file at the given `path`, creating the file if
|
||||
* necessary.
|
||||
*/
|
||||
def writeTextFile(path: Path, content: String): Unit = {
|
||||
val writer = new PrintWriter(path.toFile)
|
||||
try {
|
||||
writer.write(content)
|
||||
} finally {
|
||||
writer.close()
|
||||
}
|
||||
Using(new PrintWriter(path.toFile)) { writer => writer.write(content) }
|
||||
}
|
||||
|
||||
/**
|
||||
@ -46,12 +41,6 @@ object FileSystem {
|
||||
def copyDirectory(source: Path, destination: Path): Unit =
|
||||
FileUtils.copyDirectory(source.toFile, destination.toFile)
|
||||
|
||||
/**
|
||||
* Removes a directory recursively.
|
||||
*/
|
||||
def removeDirectory(dir: Path): Unit =
|
||||
FileUtils.deleteDirectory(dir.toFile)
|
||||
|
||||
/**
|
||||
* Copies a file, overwriting the destination if it already existed.
|
||||
*/
|
||||
@ -63,18 +52,127 @@ object FileSystem {
|
||||
*/
|
||||
def ensureIsExecutable(file: Path): Unit = {
|
||||
if (!Files.isExecutable(file)) {
|
||||
def tryChmod(): Boolean = {
|
||||
Seq("chmod", "+x", file.toAbsolutePath.toString).! == 0
|
||||
}
|
||||
|
||||
if (OS.isWindows || !tryChmod()) {
|
||||
if (OS.isWindows) {
|
||||
Logger.error("Cannot ensure the launcher binary is executable.")
|
||||
} else {
|
||||
try {
|
||||
Files.setPosixFilePermissions(
|
||||
file.toAbsolutePath,
|
||||
PosixFilePermissions.fromString("rwxrwxr-x")
|
||||
)
|
||||
} catch {
|
||||
case e: Exception =>
|
||||
Logger.error(
|
||||
s"Cannot ensure the launcher binary is executable: $e",
|
||||
e
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows to write nested paths in a more readable and concise way.
|
||||
* Parses POSIX file permissions stored in a binary format into a set of Java
|
||||
* enumerations corresponding to these permissions.
|
||||
*/
|
||||
def decodePOSIXPermissions(mode: Int): java.util.Set[PosixFilePermission] = {
|
||||
val res =
|
||||
util.EnumSet.noneOf[PosixFilePermission](classOf[PosixFilePermission])
|
||||
|
||||
val others = mode & 7
|
||||
val group = (mode >> 3) & 7
|
||||
val owner = (mode >> 6) & 7
|
||||
|
||||
if ((owner & 4) != 0) {
|
||||
res.add(PosixFilePermission.OWNER_READ)
|
||||
}
|
||||
if ((owner & 2) != 0) {
|
||||
res.add(PosixFilePermission.OWNER_WRITE)
|
||||
}
|
||||
if ((owner & 1) != 0) {
|
||||
res.add(PosixFilePermission.OWNER_EXECUTE)
|
||||
}
|
||||
|
||||
if ((group & 4) != 0) {
|
||||
res.add(PosixFilePermission.GROUP_READ)
|
||||
}
|
||||
if ((group & 2) != 0) {
|
||||
res.add(PosixFilePermission.GROUP_WRITE)
|
||||
}
|
||||
if ((group & 1) != 0) {
|
||||
res.add(PosixFilePermission.GROUP_EXECUTE)
|
||||
}
|
||||
|
||||
if ((others & 4) != 0) {
|
||||
res.add(PosixFilePermission.OTHERS_READ)
|
||||
}
|
||||
if ((others & 2) != 0) {
|
||||
res.add(PosixFilePermission.OTHERS_WRITE)
|
||||
}
|
||||
if ((others & 1) != 0) {
|
||||
res.add(PosixFilePermission.OTHERS_EXECUTE)
|
||||
}
|
||||
|
||||
res
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the `action` with a parameter representing a temporary directory
|
||||
* created for it.
|
||||
*
|
||||
* The temporary directory is removed afterwards.
|
||||
*
|
||||
* @param prefix prefix to use for the temporary directory name
|
||||
* @param action action to execute with the directory
|
||||
* @tparam T type of action's result
|
||||
* @return result of running the `action`
|
||||
*/
|
||||
def withTemporaryDirectory[T](
|
||||
prefix: String = "enso"
|
||||
)(action: Path => T): T = {
|
||||
val dir = Files.createTempDirectory(prefix)
|
||||
try {
|
||||
action(dir)
|
||||
} finally {
|
||||
removeDirectory(dir)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a directory recursively.
|
||||
*/
|
||||
def removeDirectory(dir: Path): Unit =
|
||||
FileUtils.deleteDirectory(dir.toFile)
|
||||
|
||||
/**
|
||||
* Registers the directory to be removed when the program exits normally.
|
||||
*
|
||||
* The directory is only removed if it is empty.
|
||||
*/
|
||||
def removeEmptyDirectoryOnExit(dir: Path): Unit =
|
||||
dir.toFile.deleteOnExit()
|
||||
|
||||
/**
|
||||
* Checks if the directory contains any entries.
|
||||
*/
|
||||
def isDirectoryEmpty(dir: Path): Boolean = {
|
||||
def hasEntries =
|
||||
Using(Files.newDirectoryStream(dir))(_.iterator().hasNext).get
|
||||
Files.isDirectory(dir) && !hasEntries
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to move a directory from `source` to `destination` atomically.
|
||||
*
|
||||
* May not be actually atomic.
|
||||
*/
|
||||
def atomicMove(source: Path, destination: Path): Unit = {
|
||||
Files.createDirectories(destination.getParent)
|
||||
Files.move(source, destination, StandardCopyOption.ATOMIC_MOVE)
|
||||
}
|
||||
|
||||
/**
|
||||
* Syntax allowing to write nested paths in a more readable and concise way.
|
||||
*/
|
||||
implicit class PathSyntax(val path: Path) extends AnyVal {
|
||||
def /(other: String): Path = path.resolve(other)
|
||||
|
@ -5,11 +5,121 @@ import java.nio.file.Path
|
||||
import org.enso.launcher.installation.DistributionManager
|
||||
import org.enso.pkg.PackageManager
|
||||
import org.enso.version.{VersionDescription, VersionDescriptionParameter}
|
||||
import buildinfo.Info
|
||||
import nl.gn0s1s.bump.SemVer
|
||||
import org.enso.launcher.cli.GlobalCLIOptions
|
||||
import org.enso.launcher.components.DefaultComponentsManager
|
||||
|
||||
/**
|
||||
* Implements launcher commands that are run from CLI and can be affected by
|
||||
* the global CLI options.
|
||||
*
|
||||
* @param cliOptions the global CLI options to use for the commands
|
||||
*/
|
||||
case class Launcher(cliOptions: GlobalCLIOptions) {
|
||||
private lazy val componentsManager = DefaultComponentsManager(cliOptions)
|
||||
|
||||
/**
|
||||
* Prints a list of installed engines.
|
||||
*/
|
||||
def listEngines(): Unit = {
|
||||
for (engine <- componentsManager.listInstalledEngines()) {
|
||||
println(engine.version.toString)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prints a list of installed runtimes.
|
||||
*/
|
||||
def listRuntimes(): Unit = {
|
||||
for (runtime <- componentsManager.listInstalledRuntimes()) {
|
||||
val engines = componentsManager.findEnginesUsingRuntime(runtime)
|
||||
val usedBy = {
|
||||
val plural =
|
||||
if (engines.length != 1) "s"
|
||||
else ""
|
||||
s"(used by ${engines.length} Enso installation$plural)"
|
||||
}
|
||||
println(s"$runtime $usedBy")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prints a summary of installed components and their dependencies.
|
||||
*/
|
||||
def listSummary(): Unit = {
|
||||
for (engine <- componentsManager.listInstalledEngines()) {
|
||||
val runtime = componentsManager.findRuntime(engine)
|
||||
val runtimeName = runtime
|
||||
.map(_.toString)
|
||||
.getOrElse("no runtime found for this distribution")
|
||||
println(s"Enso ${engine.version} -> $runtimeName")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Installs the specified engine `version`.
|
||||
*
|
||||
* Also installs the required runtime if it wasn't already installed.
|
||||
*/
|
||||
def installEngine(version: SemVer): Unit = {
|
||||
val existing = componentsManager.findEngine(version)
|
||||
if (existing.isDefined) {
|
||||
Logger.info(s"Engine $version is already installed.")
|
||||
} else {
|
||||
componentsManager.findOrInstallEngine(version, complain = false)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Installs the latest available version of the engine.
|
||||
*
|
||||
* Also installs the required runtime if it wasn't already installed.
|
||||
*/
|
||||
def installLatestEngine(): Unit = {
|
||||
val latest = componentsManager.fetchLatestEngineVersion()
|
||||
Logger.info(s"Installing Enso engine $latest")
|
||||
installEngine(latest)
|
||||
}
|
||||
|
||||
/**
|
||||
* Uninstalls the specified engine `version`.
|
||||
*
|
||||
* If a runtime is not used by any engines anymore, it is also removed.
|
||||
*/
|
||||
def uninstallEngine(version: SemVer): Unit =
|
||||
componentsManager.uninstallEngine(version)
|
||||
|
||||
def runRepl(
|
||||
pathHint: Option[Path],
|
||||
jvmArguments: Seq[(String, String)],
|
||||
additionalArguments: Seq[String]
|
||||
): Unit = {
|
||||
// TODO [RW] this is just a stub, it will be implemented in #976
|
||||
val path = pathHint.getOrElse(Path.of(".")).toAbsolutePath
|
||||
val detectedVersion = SemVer(0, 1, 0) // TODO [RW] default version etc.
|
||||
val engine = componentsManager.findOrInstallEngine(detectedVersion)
|
||||
val runtime = componentsManager.findOrInstallRuntime(engine)
|
||||
println(s"Will launch the REPL in $path")
|
||||
println(s"with $engine with additional arguments $additionalArguments")
|
||||
println(s"using $runtime with JVM arguments $jvmArguments")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gathers launcher commands which do not depend on the global CLI options.
|
||||
*/
|
||||
object Launcher {
|
||||
private val packageManager = PackageManager.Default
|
||||
|
||||
private def workingDirectory: Path = Path.of(".")
|
||||
/**
|
||||
* Version of the launcher.
|
||||
*/
|
||||
val version: SemVer = SemVer(Info.ensoVersion).getOrElse {
|
||||
throw new IllegalStateException("Cannot parse the built-in version.")
|
||||
}
|
||||
|
||||
private val packageManager = PackageManager.Default
|
||||
private val workingDirectory: Path = Path.of(".")
|
||||
|
||||
/**
|
||||
* Creates a new project with the given `name` in the given `path`.
|
||||
@ -23,7 +133,7 @@ object Launcher {
|
||||
def newProject(name: String, path: Option[Path]): Unit = {
|
||||
val actualPath = path.getOrElse(workingDirectory.resolve(name))
|
||||
packageManager.create(actualPath.toFile, name)
|
||||
println(s"Project created in $actualPath")
|
||||
Logger.info(s"Project created in $actualPath")
|
||||
}
|
||||
|
||||
/**
|
||||
@ -62,5 +172,4 @@ object Launcher {
|
||||
sys.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -14,16 +14,26 @@ object Logger {
|
||||
private val Warning = Level("warn", 3)
|
||||
private val Error = Level("error", 4)
|
||||
|
||||
private val logLevel = Warning
|
||||
private var logLevel = Info
|
||||
private def log(level: Level, msg: => String): Unit =
|
||||
if (level.level >= logLevel.level)
|
||||
System.err.println(s"[${level.name}] $msg")
|
||||
if (level.level >= logLevel.level) {
|
||||
System.out.println(s"[${level.name}] $msg")
|
||||
System.out.flush()
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs a debug level message.
|
||||
*/
|
||||
def debug(msg: => String): Unit = log(Debug, msg)
|
||||
|
||||
/**
|
||||
* Logs a debug level message and attaches a stack trace.
|
||||
*/
|
||||
def debug(msg: => String, throwable: => Throwable): Unit = {
|
||||
log(Debug, msg)
|
||||
trace(throwable)
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs an info level message.
|
||||
*/
|
||||
@ -54,4 +64,18 @@ object Logger {
|
||||
def trace(throwable: => Throwable): Unit =
|
||||
if (Debug.level >= logLevel.level)
|
||||
throwable.printStackTrace()
|
||||
|
||||
/**
|
||||
* Runs the provided action with a log level that will allow only for errors
|
||||
* and returns its result.
|
||||
*/
|
||||
def suppressWarnings[R](action: => R): R = {
|
||||
val oldLevel = logLevel
|
||||
try {
|
||||
logLevel = Error
|
||||
action
|
||||
} finally {
|
||||
logLevel = oldLevel
|
||||
}
|
||||
}
|
||||
}
|
||||
|
93
engine/launcher/src/main/scala/org/enso/launcher/OS.scala
Normal file
93
engine/launcher/src/main/scala/org/enso/launcher/OS.scala
Normal file
@ -0,0 +1,93 @@
|
||||
package org.enso.launcher
|
||||
|
||||
sealed trait OS {
|
||||
def name: String
|
||||
}
|
||||
object OS {
|
||||
case object Linux extends OS {
|
||||
def name: String = "linux"
|
||||
}
|
||||
case object MacOS extends OS {
|
||||
def name: String = "mac"
|
||||
}
|
||||
case object Windows extends OS {
|
||||
def name: String = "windows"
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the application is being run on Windows.
|
||||
*/
|
||||
def isWindows: Boolean =
|
||||
operatingSystem == OS.Windows
|
||||
|
||||
def isUNIX: Boolean =
|
||||
operatingSystem == OS.Linux || operatingSystem == OS.MacOS
|
||||
|
||||
/**
|
||||
* Returns which [[OS]] this program is running on.
|
||||
*/
|
||||
lazy val operatingSystem: OS = detectOS
|
||||
|
||||
private val ENSO_OPERATING_SYSTEM = "ENSO_OPERATING_SYSTEM"
|
||||
|
||||
private def detectOS: OS = {
|
||||
val knownOS = Seq(Linux, MacOS, Windows)
|
||||
val overridenName = Option(System.getenv(ENSO_OPERATING_SYSTEM))
|
||||
overridenName match {
|
||||
case Some(value) =>
|
||||
knownOS.find(_.name == value.toLowerCase) match {
|
||||
case Some(overriden) =>
|
||||
Logger.debug(
|
||||
s"OS overriden by $ENSO_OPERATING_SYSTEM to $overriden."
|
||||
)
|
||||
return overriden
|
||||
case None =>
|
||||
Logger.warn(
|
||||
s"$ENSO_OPERATING_SYSTEM is set to an unknown value $value, " +
|
||||
s"ignoring."
|
||||
)
|
||||
}
|
||||
case None =>
|
||||
}
|
||||
|
||||
val name = System.getProperty("os.name").toLowerCase
|
||||
def nameMatches(os: OS): Boolean = name.contains(os.name)
|
||||
val possibleOS = knownOS.filter(nameMatches)
|
||||
if (possibleOS.length == 1) {
|
||||
possibleOS.head
|
||||
} else {
|
||||
Logger.error(
|
||||
s"Could not determine a supported operating system. Please make sure " +
|
||||
s"the OS you are running is supported. You can try to manually " +
|
||||
s"override the operating system detection by setting an environment " +
|
||||
s"variable `$ENSO_OPERATING_SYSTEM` to one of the possible values " +
|
||||
s"`linux`, `mac`, `windows` depending on the system that your OS " +
|
||||
s"most behaves like."
|
||||
)
|
||||
throw new IllegalStateException(
|
||||
"fatal: Could not detect the operating system."
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Name of the architecture that the program is running on.
|
||||
*
|
||||
* Currently the Launcher Native Image builds only support amd64
|
||||
* architecture, so it is hardcoded here. In the future, more architectures
|
||||
* may be supported. In that case, this will need to be updated to get the
|
||||
* target architecture from the build settings.
|
||||
*
|
||||
* This property should not use `System.getProperty("os.arch")` directly
|
||||
* because it can return different values for the same architecture (for
|
||||
* example on some systems `amd64` is called `x86_64`).
|
||||
*/
|
||||
val architecture: String = "amd64"
|
||||
|
||||
/**
|
||||
* Wraps the base executable name with an optional platform-dependent
|
||||
* extension.
|
||||
*/
|
||||
def executableName(baseName: String): String =
|
||||
if (isWindows) baseName + ".exe" else baseName
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
package org.enso.launcher
|
||||
|
||||
/**
|
||||
* Default implementation of the [[internal.PluginManager]] using the default
|
||||
* [[Environment]].
|
||||
*/
|
||||
object PluginManager extends internal.PluginManager(Environment)
|
@ -0,0 +1,216 @@
|
||||
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.tar.{
|
||||
TarArchiveEntry,
|
||||
TarArchiveInputStream
|
||||
}
|
||||
import org.apache.commons.compress.archivers.zip.{
|
||||
ZipArchiveEntry,
|
||||
ZipArchiveInputStream
|
||||
}
|
||||
import org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream
|
||||
import org.apache.commons.io.IOUtils
|
||||
import org.enso.cli.{TaskProgress, TaskProgressImplementation}
|
||||
import org.enso.launcher.archive.internal.{ArchiveIterator, BaseRenamer}
|
||||
import org.enso.launcher.internal.ReadProgress
|
||||
import org.enso.launcher.{FileSystem, Logger, OS}
|
||||
|
||||
import scala.util.{Try, Using}
|
||||
|
||||
/**
|
||||
* Contains utilities related to the extraction of various archive file
|
||||
* formats.
|
||||
*
|
||||
* Currently it supports extracting ZIP files and gzipped TAR archives.
|
||||
*/
|
||||
object Archive {
|
||||
|
||||
/**
|
||||
* Extracts the archive at `archivePath` to `destinationDirectory`.
|
||||
*
|
||||
* Behaves in the same way as the other [[extractArchive]] overload, but it
|
||||
* tries to determine the archive format automatically based on the filename.
|
||||
*
|
||||
* @param archivePath path to the archive file
|
||||
* @param destinationDirectory directory into which the archive contents
|
||||
* will be extracted
|
||||
* @param renameRootFolder if not None, the root folder of the archive will
|
||||
* be renamed to the provided name. In such case the
|
||||
* archive must contain only one root folder.
|
||||
* @return an instance indicating the progress of extraction
|
||||
*/
|
||||
def extractArchive(
|
||||
archivePath: Path,
|
||||
destinationDirectory: Path,
|
||||
renameRootFolder: Option[Path]
|
||||
): TaskProgress[Unit] = {
|
||||
val format = ArchiveFormat.detect(archivePath)
|
||||
format
|
||||
.map(
|
||||
extractArchive(archivePath, _, destinationDirectory, renameRootFolder)
|
||||
)
|
||||
.getOrElse {
|
||||
TaskProgress.immediateFailure(
|
||||
new IllegalArgumentException(
|
||||
s"Could not detect archive format for " +
|
||||
s"${archivePath.getFileName}"
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the archive at `archivePath` to `destinationDirectory`.
|
||||
*
|
||||
* The extraction is run in a background thread, the function returns
|
||||
* immediately with a [[TaskProgress]] instance that can be used to track
|
||||
* extraction progress (for example by displaying a progress bar or just
|
||||
* waiting for it to complete).
|
||||
*
|
||||
* If `renameRootFolder` is provided, the root folder of the archive is
|
||||
* renamed to the provided value. It is an error to request root folder
|
||||
* rename for an archive that has more than one file in the root.
|
||||
*
|
||||
* @param archivePath path to the archive file
|
||||
* @param format format of the archive
|
||||
* @param destinationDirectory directory into which the archive contents
|
||||
* will be extracted
|
||||
* @param renameRootFolder if not None, the root folder of the archive will
|
||||
* be renamed to the provided name. In such case the
|
||||
* archive must contain only one root folder.
|
||||
* @return an instance indicating the progress of extraction
|
||||
*/
|
||||
def extractArchive(
|
||||
archivePath: Path,
|
||||
format: ArchiveFormat,
|
||||
destinationDirectory: Path,
|
||||
renameRootFolder: Option[Path]
|
||||
): 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`.")
|
||||
var missingPermissions: Int = 0
|
||||
|
||||
val result = withOpenArchive(archivePath, format) { (archive, progress) =>
|
||||
for (entry <- ArchiveIterator(archive)) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
if (OS.isUNIX) {
|
||||
getMode(entry) match {
|
||||
case Some(mode) =>
|
||||
val permissions = FileSystem.decodePOSIXPermissions(mode)
|
||||
Files.setPosixFilePermissions(destinationPath, permissions)
|
||||
case None =>
|
||||
missingPermissions += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
taskProgress.reportProgress(
|
||||
progress.alreadyRead(),
|
||||
progress.total()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (missingPermissions > 0) {
|
||||
Logger.warn(
|
||||
s"Could not find permissions for $missingPermissions files in " +
|
||||
s"archive `$archivePath`, some files may not have been marked as " +
|
||||
s"executable."
|
||||
)
|
||||
}
|
||||
|
||||
taskProgress.setComplete(result)
|
||||
}
|
||||
val thread = new Thread(() => runExtraction(), "Extracting-Archive")
|
||||
thread.start()
|
||||
taskProgress
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to get the POSIX file permissions associated with that `entry`.
|
||||
*/
|
||||
private def getMode(entry: ArchiveEntry): Option[Int] =
|
||||
entry match {
|
||||
case entry: TarArchiveEntry =>
|
||||
Some(entry.getMode)
|
||||
case entry: ZipArchiveEntry =>
|
||||
if (entry.getPlatform == ZipArchiveEntry.PLATFORM_UNIX)
|
||||
Some(entry.getUnixMode)
|
||||
else None
|
||||
case _ =>
|
||||
None
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the archive at `path` and executes the provided action.
|
||||
*
|
||||
* The action is given an [[ArchiveInputStream]] that can be used to read
|
||||
* from the archive and a [[ReadProgress]] instance which indicates how many
|
||||
* bytes have already been read at a given moment.
|
||||
*
|
||||
* The archive and the internal streams are closed when this function exits.
|
||||
* The `action` can throw an exception, in which case a failure is returned.
|
||||
*/
|
||||
def withOpenArchive[R](path: Path, format: ArchiveFormat)(
|
||||
action: (ArchiveInputStream, ReadProgress) => R
|
||||
): Try[R] = {
|
||||
Using(FileProgressInputStream(path)) { progressInputStream =>
|
||||
Using(new BufferedInputStream(progressInputStream)) { buffered =>
|
||||
format match {
|
||||
case ArchiveFormat.ZIP =>
|
||||
Using(new ZipArchiveInputStream(buffered))(
|
||||
action(_, progressInputStream.progress)
|
||||
).get
|
||||
case ArchiveFormat.TarGz =>
|
||||
Using(new GzipCompressorInputStream(buffered)) {
|
||||
compressorInputStream =>
|
||||
Using(new TarArchiveInputStream(compressorInputStream))(
|
||||
action(_, progressInputStream.progress)
|
||||
).get
|
||||
}.get
|
||||
}
|
||||
}.get
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the name of the entry inside of the archive into a relative path.
|
||||
*
|
||||
* The specification does not restrict entry names to valid paths, but
|
||||
* empirical testing shows that paths in the archives we need to support are
|
||||
* usually separated by '/' which is correctly parsed into a system-dependent
|
||||
* path both on UNIX and Windows platforms.
|
||||
*/
|
||||
private def parseArchiveEntryName(name: String): Path =
|
||||
Path.of(name)
|
||||
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
package org.enso.launcher.archive
|
||||
|
||||
import java.nio.file.Path
|
||||
|
||||
/**
|
||||
* Enumeration of supported archive formats.
|
||||
*/
|
||||
sealed trait ArchiveFormat
|
||||
object ArchiveFormat {
|
||||
|
||||
/**
|
||||
* ZIP file format.
|
||||
*/
|
||||
case object ZIP extends ArchiveFormat
|
||||
|
||||
/**
|
||||
* Represents a gzipped TAR archive.
|
||||
*/
|
||||
case object TarGz extends ArchiveFormat
|
||||
|
||||
/**
|
||||
* Tries to infer one of the supported archive formats based on the filename.
|
||||
*
|
||||
* Returns None if it is unable to determine a known format.
|
||||
*/
|
||||
def detect(path: Path): Option[ArchiveFormat] = {
|
||||
val fileName = path.getFileName.toString
|
||||
if (fileName.endsWith(".zip")) Some(ZIP)
|
||||
else if (fileName.endsWith(".tar.gz") || fileName.endsWith(".tgz"))
|
||||
Some(TarGz)
|
||||
else None
|
||||
}
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
package org.enso.launcher.archive
|
||||
|
||||
import java.io.FileInputStream
|
||||
import java.nio.file.{Files, Path}
|
||||
|
||||
import org.enso.launcher.internal.ProgressInputStream
|
||||
|
||||
/**
|
||||
* A helper that allows to create a [[ProgressInputStream]] for a file located
|
||||
* at the given path.
|
||||
*/
|
||||
object FileProgressInputStream {
|
||||
|
||||
/**
|
||||
* Creates a [[ProgressInputStream]] reading from the file at `path`.
|
||||
*
|
||||
* The read progress depends on how many bytes have been read from the file.
|
||||
* The total size is determined from the file size as returned by the
|
||||
* filesystem.
|
||||
*/
|
||||
def apply(path: Path): ProgressInputStream = {
|
||||
val size = Files.size(path)
|
||||
new ProgressInputStream(
|
||||
new FileInputStream(path.toFile),
|
||||
Some(size),
|
||||
_ => ()
|
||||
)
|
||||
}
|
||||
}
|
@ -0,0 +1,52 @@
|
||||
package org.enso.launcher.archive.internal
|
||||
|
||||
import org.apache.commons.compress.archivers.{ArchiveEntry, ArchiveInputStream}
|
||||
|
||||
/**
|
||||
* Wraps an [[ArchiveInputStream]] to get an [[Iterator]] which produces
|
||||
* non-null archive entries.
|
||||
*/
|
||||
case class ArchiveIterator(
|
||||
archiveInputStream: ArchiveInputStream
|
||||
) extends Iterator[ArchiveEntry] {
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
override def hasNext: Boolean = {
|
||||
findNext()
|
||||
nextEntry.isDefined
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
override def next(): ArchiveEntry = {
|
||||
findNext()
|
||||
nextEntry match {
|
||||
case Some(value) =>
|
||||
nextEntry = None
|
||||
value
|
||||
case None =>
|
||||
throw new NoSuchElementException("No more entries in the iterator.")
|
||||
}
|
||||
}
|
||||
|
||||
private var nextEntry: Option[ArchiveEntry] = None
|
||||
private var finished: Boolean = false
|
||||
|
||||
/**
|
||||
* Tries to move to the next entry. If it is `null` then it means the
|
||||
* [[archiveInputStream]] has run out of entries.
|
||||
*/
|
||||
private def findNext(): Unit = {
|
||||
if (nextEntry.isEmpty && !finished) {
|
||||
val nextCandidate = archiveInputStream.getNextEntry
|
||||
if (nextCandidate == null) {
|
||||
finished = true
|
||||
} else {
|
||||
nextEntry = Some(nextCandidate)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,50 @@
|
||||
package org.enso.launcher.archive.internal
|
||||
|
||||
import java.nio.file.Path
|
||||
|
||||
/**
|
||||
* Acts as a function that renames the base of the provided paths to the
|
||||
* `newBase`.
|
||||
*
|
||||
* It changes the first component of the provided paths to the provided
|
||||
* `newBase`. It is meant to be used for renaming root directories of extracted
|
||||
* archives. The provided paths should be relative.
|
||||
*
|
||||
* It ensures that all paths converted with this function have the same base,
|
||||
* to avoid merging two base directories.
|
||||
*/
|
||||
class BaseRenamer(newBase: Path) extends (Path => Path) {
|
||||
var lastRoot: Option[Path] = None
|
||||
|
||||
/**
|
||||
* Changes the path by renaming its base component.
|
||||
*/
|
||||
override def apply(path: Path): Path = {
|
||||
if (path.getNameCount < 1) {
|
||||
throw new RuntimeException(
|
||||
s"Unexpected archive structure - the file $path cannot " +
|
||||
s"have its root renamed."
|
||||
)
|
||||
}
|
||||
|
||||
val oldRoot = path.getName(0)
|
||||
lastRoot match {
|
||||
case Some(lastRoot) =>
|
||||
if (lastRoot != oldRoot) {
|
||||
throw new RuntimeException(
|
||||
s"Unexpected archive structure - the archive contains " +
|
||||
s"more than one file in the root - $lastRoot, $oldRoot"
|
||||
)
|
||||
}
|
||||
case None =>
|
||||
lastRoot = Some(oldRoot)
|
||||
}
|
||||
|
||||
if (path.getNameCount == 1)
|
||||
newBase
|
||||
else {
|
||||
val remainingParts = path.subpath(1, path.getNameCount)
|
||||
newBase.resolve(remainingParts)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
package org.enso.launcher.cli
|
||||
|
||||
import nl.gn0s1s.bump.SemVer
|
||||
import org.enso.cli.Argument
|
||||
|
||||
object Arguments {
|
||||
|
||||
/**
|
||||
* [[Argument]] instance that tries to parse the String as a [[SemVer]]
|
||||
* version string.
|
||||
*/
|
||||
implicit val semverArgument: Argument[SemVer] = (string: String) =>
|
||||
SemVer(string).toRight(
|
||||
List(s"`$string` is not a valid semantic version string.")
|
||||
)
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
package org.enso.launcher.cli
|
||||
|
||||
/**
|
||||
* Gathers settings set by the global CLI options.
|
||||
*
|
||||
* @param autoConfirm if this flag is set, the program should not ask the user
|
||||
* any questions but proceed with the default values, that
|
||||
* must be explained in the help text for each command
|
||||
* @param hideProgress if this flag is set, progress bars should not be
|
||||
* printed
|
||||
*/
|
||||
case class GlobalCLIOptions(autoConfirm: Boolean, hideProgress: Boolean)
|
@ -1,4 +1,4 @@
|
||||
package org.enso.launcher
|
||||
package org.enso.launcher.cli
|
||||
|
||||
import java.io.IOException
|
||||
import java.nio.file.{Files, NoSuchFileException, Path}
|
||||
@ -6,8 +6,29 @@ import java.nio.file.{Files, NoSuchFileException, Path}
|
||||
import org.enso.cli.Opts
|
||||
import org.enso.cli.Opts.implicits._
|
||||
import cats.implicits._
|
||||
import org.enso.launcher.internal.OS
|
||||
import org.enso.launcher.OS
|
||||
|
||||
/**
|
||||
* Implements internal options that the launcher may use when running another
|
||||
* instance of itself.
|
||||
*
|
||||
* These options are used primarily to implement workarounds for
|
||||
* 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:
|
||||
*
|
||||
* 1. Remove Old Executable
|
||||
* On Windows, if an executable is running, its file is locked, so it is
|
||||
* impossible to remove a file that is running. Thus it is not possible for
|
||||
* the launcher to directly remove its old executable when it finishes the
|
||||
* installation. Instead it launches the newly installed launcher, which
|
||||
* tries to remove it, and terminates itself. Thus the executable lock is
|
||||
* soon freed and the new launcher is able to remove the old one. The new
|
||||
* launcher attempts several retries, because it may take some time for the
|
||||
* executable to be unlocked (especially as on Windows software like an
|
||||
* antivirus may block it for some more time after terminating).
|
||||
*/
|
||||
object InternalOpts {
|
||||
private val REMOVE_OLD_EXECUTABLE = "internal-remove-old-executable"
|
||||
|
@ -1,25 +1,23 @@
|
||||
package org.enso.launcher
|
||||
package org.enso.launcher.cli
|
||||
|
||||
import java.nio.file.Path
|
||||
import java.util.UUID
|
||||
|
||||
import org.enso.cli.{
|
||||
Application,
|
||||
Argument,
|
||||
CLIOutput,
|
||||
Command,
|
||||
Opts,
|
||||
Subcommand,
|
||||
TopLevelBehavior
|
||||
}
|
||||
import org.enso.cli.Opts.implicits._
|
||||
import cats.implicits._
|
||||
import nl.gn0s1s.bump.SemVer
|
||||
import org.enso.cli.Opts.implicits._
|
||||
import org.enso.cli._
|
||||
import org.enso.launcher.cli.Arguments._
|
||||
import org.enso.launcher.installation.DistributionInstaller.BundleAction
|
||||
import org.enso.launcher.installation.{
|
||||
DistributionInstaller,
|
||||
DistributionManager
|
||||
}
|
||||
import org.enso.launcher.installation.DistributionInstaller.BundleAction
|
||||
import org.enso.launcher.{Launcher, Logger}
|
||||
|
||||
/**
|
||||
* Defines the CLI commands and options for the program and its entry point.
|
||||
*/
|
||||
object Main {
|
||||
private def jsonFlag(showInUsage: Boolean): Opts[Boolean] =
|
||||
Opts.flag(
|
||||
@ -28,7 +26,7 @@ object Main {
|
||||
showInUsage
|
||||
)
|
||||
|
||||
type Config = Unit
|
||||
type Config = GlobalCLIOptions
|
||||
|
||||
private def versionCommand: Command[Config => Unit] =
|
||||
Command(
|
||||
@ -63,7 +61,9 @@ object Main {
|
||||
private def runCommand: Command[Config => Unit] =
|
||||
Command(
|
||||
"run",
|
||||
"Run a project or Enso script.",
|
||||
"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](
|
||||
@ -86,7 +86,9 @@ object Main {
|
||||
private def languageServerCommand: Command[Config => Unit] =
|
||||
Command(
|
||||
"language-server",
|
||||
"Launch the Language Server for a given project.",
|
||||
"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.")
|
||||
@ -131,13 +133,17 @@ object Main {
|
||||
}
|
||||
|
||||
private def replCommand: Command[Config => Unit] =
|
||||
Command("repl", "Launch an Enso REPL.") {
|
||||
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", "Project path.")
|
||||
val additionalArgs = Opts.additionalArguments()
|
||||
(path, jvmArgs, additionalArgs) mapN {
|
||||
(path, jvmArgs, additionalArgs) => (_: Config) =>
|
||||
println(s"Launch REPL in $path")
|
||||
println(s"JVM=$jvmArgs, additionalArgs=$additionalArgs")
|
||||
(path, jvmArgs, additionalArgs) => (config: Config) =>
|
||||
Launcher(config).runRepl(path, jvmArgs, additionalArgs)
|
||||
}
|
||||
}
|
||||
|
||||
@ -173,23 +179,24 @@ object Main {
|
||||
|
||||
private def installEngineCommand: Subcommand[Config => Unit] =
|
||||
Subcommand("engine") {
|
||||
val version = Opts.positionalArgument[String]("VERSION")
|
||||
version map { version => (_: Config) =>
|
||||
println(s"Install $version")
|
||||
val version = Opts.optionalArgument[SemVer](
|
||||
"VERSION",
|
||||
"The version to install. If not provided, the latest version is " +
|
||||
"installed."
|
||||
)
|
||||
version map { version => (config: Config) =>
|
||||
version match {
|
||||
case Some(value) =>
|
||||
Launcher(config).installEngine(value)
|
||||
case None =>
|
||||
Launcher(config).installLatestEngine()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private def installDistributionCommand: Subcommand[Config => Unit] =
|
||||
Subcommand("distribution") {
|
||||
val autoConfirm = Opts.flag(
|
||||
"auto-confirm",
|
||||
"Proceeds with installation without asking confirmation questions. " +
|
||||
"If bundled components are present, this flag will move them by " +
|
||||
"default, unless overridden by an explicit setting of " +
|
||||
"`--install-bundle-mode`. On success, the installer will remove " +
|
||||
"itself to avoid conflicts with the installed launcher executable.",
|
||||
showInUsage = false
|
||||
)
|
||||
|
||||
implicit val bundleActionParser: Argument[BundleAction] = {
|
||||
case "move" => DistributionInstaller.MoveBundles.asRight
|
||||
case "copy" => DistributionInstaller.CopyBundles.asRight
|
||||
@ -207,14 +214,25 @@ object Main {
|
||||
"If `auto-confirm` is set, defaults to move.",
|
||||
showInUsage = false
|
||||
)
|
||||
(autoConfirm, bundleAction) mapN {
|
||||
(autoConfirm, bundleAction) => (_: Config) =>
|
||||
|
||||
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,
|
||||
autoConfirm,
|
||||
if (autoConfirm)
|
||||
Some(bundleAction.getOrElse(DistributionInstaller.MoveBundles))
|
||||
else bundleAction
|
||||
config.autoConfirm,
|
||||
removeOldLauncher = !doNotRemoveOldLauncher,
|
||||
bundleActionOption =
|
||||
if (config.autoConfirm)
|
||||
Some(bundleAction.getOrElse(DistributionInstaller.MoveBundles))
|
||||
else bundleAction
|
||||
).install()
|
||||
}
|
||||
}
|
||||
@ -227,39 +245,55 @@ object Main {
|
||||
Opts.subcommands(installEngineCommand, installDistributionCommand)
|
||||
}
|
||||
|
||||
private def uninstallCommand: Command[Config => Unit] =
|
||||
Command("uninstall", "Uninstall an Enso version.") {
|
||||
val version = Opts.positionalArgument[String]("VERSION")
|
||||
version map { version => (_: Config) =>
|
||||
println(s"Uninstall $version")
|
||||
private def uninstallEngineCommand: Subcommand[Config => Unit] =
|
||||
Subcommand("engine") {
|
||||
val version = Opts.positionalArgument[SemVer]("VERSION")
|
||||
version map { version => (config: Config) =>
|
||||
Launcher(config).uninstallEngine(version)
|
||||
}
|
||||
}
|
||||
|
||||
private def uninstallDistributionCommand: Subcommand[Config => Unit] =
|
||||
Subcommand("distribution") {
|
||||
Opts.pure(()) map { (_: Unit) => (_: Config) =>
|
||||
Logger.error("Not implemented yet.")
|
||||
sys.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
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 "enso" => EnsoComponents.asRight
|
||||
case "engine" => EnsoComponents.asRight
|
||||
case "runtime" => RuntimeComponents.asRight
|
||||
case other =>
|
||||
List(
|
||||
s"Unknown argument `$other` - expected `enso`, `runtime` " +
|
||||
s"Unknown argument `$other` - expected `engine`, `runtime` " +
|
||||
"or no argument to print a general summary."
|
||||
).asLeft
|
||||
}
|
||||
|
||||
val what = Opts.optionalArgument[Components](
|
||||
"COMPONENT",
|
||||
"COMPONENT can be either `enso`, `runtime` or none. " +
|
||||
"COMPONENT can be either `engine`, `runtime` or none. " +
|
||||
"If not specified, prints a summary of all installed components."
|
||||
)
|
||||
what map { what => (_: Config) =>
|
||||
what map { what => (config: Config) =>
|
||||
what match {
|
||||
case Some(EnsoComponents) => println("List enso")
|
||||
case Some(RuntimeComponents) => println("List runtime")
|
||||
case None => println("List summary")
|
||||
case Some(EnsoComponents) => Launcher(config).listEngines()
|
||||
case Some(RuntimeComponents) => Launcher(config).listRuntimes()
|
||||
case None => Launcher(config).listSummary()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -294,10 +328,39 @@ object Main {
|
||||
"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, help, version, json, ensurePortable) mapN {
|
||||
(_, help, version, useJSON, shouldEnsurePortable) => () =>
|
||||
(
|
||||
internalOpts,
|
||||
help,
|
||||
version,
|
||||
json,
|
||||
ensurePortable,
|
||||
autoConfirm,
|
||||
hideProgress
|
||||
) mapN {
|
||||
(
|
||||
_,
|
||||
help,
|
||||
version,
|
||||
useJSON,
|
||||
shouldEnsurePortable,
|
||||
autoConfirm,
|
||||
hideProgress
|
||||
) => () =>
|
||||
if (shouldEnsurePortable) {
|
||||
Launcher.ensurePortable()
|
||||
}
|
||||
@ -308,7 +371,13 @@ object Main {
|
||||
} else if (version) {
|
||||
Launcher.displayVersion(useJSON)
|
||||
TopLevelBehavior.Halt
|
||||
} else TopLevelBehavior.Continue(())
|
||||
} else
|
||||
TopLevelBehavior.Continue(
|
||||
GlobalCLIOptions(
|
||||
autoConfirm = autoConfirm,
|
||||
hideProgress = hideProgress
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -339,17 +408,29 @@ object Main {
|
||||
CLIOutput.println(application.renderHelp())
|
||||
}
|
||||
|
||||
private def setup(): Unit =
|
||||
System.setProperty(
|
||||
"org.apache.commons.logging.Log",
|
||||
"org.apache.commons.logging.impl.NoOpLog"
|
||||
)
|
||||
|
||||
def main(args: Array[String]): Unit = {
|
||||
try {
|
||||
application.run(args) match {
|
||||
case Left(errors) =>
|
||||
CLIOutput.println(errors.mkString("\n"))
|
||||
sys.exit(1)
|
||||
case Right(()) =>
|
||||
setup()
|
||||
val exitCode =
|
||||
try {
|
||||
application.run(args) match {
|
||||
case Left(errors) =>
|
||||
CLIOutput.println(errors.mkString("\n"))
|
||||
1
|
||||
case Right(()) =>
|
||||
0
|
||||
}
|
||||
} catch {
|
||||
case e: Exception =>
|
||||
Logger.error(s"A fatal error has occurred: $e", e)
|
||||
1
|
||||
}
|
||||
} catch {
|
||||
case e: RuntimeException =>
|
||||
Logger.error(s"A fatal error has occurred: $e", e)
|
||||
}
|
||||
|
||||
sys.exit(exitCode)
|
||||
}
|
||||
}
|
@ -1,9 +1,9 @@
|
||||
package org.enso.launcher.internal
|
||||
package org.enso.launcher.cli
|
||||
|
||||
import java.nio.file.{Files, Path}
|
||||
|
||||
import org.enso.cli.{CommandHelp, PluginBehaviour, PluginNotFound}
|
||||
import org.enso.launcher.FileSystem
|
||||
import org.enso.launcher.{Environment, FileSystem}
|
||||
|
||||
import scala.sys.process._
|
||||
import scala.util.Try
|
||||
@ -117,3 +117,9 @@ class PluginManager(env: Environment) extends org.enso.cli.PluginManager {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Default implementation of the [[PluginManager]] using the default
|
||||
* [[Environment]].
|
||||
*/
|
||||
object PluginManager extends PluginManager(Environment)
|
@ -0,0 +1,58 @@
|
||||
package org.enso.launcher.components
|
||||
|
||||
import nl.gn0s1s.bump.SemVer
|
||||
import org.enso.launcher.Launcher
|
||||
|
||||
/**
|
||||
* A base class for exceptions caused by [[ComponentsManager]] logic.
|
||||
*/
|
||||
sealed class ComponentsException(message: String, cause: Throwable = null)
|
||||
extends RuntimeException(message, cause) {
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
override def toString: String = {
|
||||
val causeMessage = if (cause != null) s" (Caused by: $cause)" else ""
|
||||
message + causeMessage
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents an installation failure.
|
||||
*/
|
||||
case class InstallationError(message: String, cause: Throwable = null)
|
||||
extends ComponentsException(message, cause) {
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
override def toString: String = s"Installation failed: $message"
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates that a requested engine version requires a newer launcher version.
|
||||
*/
|
||||
case class LauncherUpgradeRequiredError(expectedVersion: SemVer)
|
||||
extends ComponentsException(
|
||||
s"Minimum launcher version required to use this engine is " +
|
||||
s"$expectedVersion"
|
||||
) {
|
||||
|
||||
/**
|
||||
* @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`."
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates a component is not recognized.
|
||||
*/
|
||||
case class UnrecognizedComponentError(message: String, cause: Throwable = null)
|
||||
extends ComponentsException(message, cause)
|
||||
|
||||
case class ComponentMissingError(message: String, cause: Throwable = null)
|
||||
extends ComponentsException(message, cause)
|
@ -0,0 +1,553 @@
|
||||
package org.enso.launcher.components
|
||||
|
||||
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
|
||||
import org.enso.launcher.installation.DistributionManager
|
||||
import org.enso.launcher.releases.{
|
||||
EngineReleaseProvider,
|
||||
GraalCEReleaseProvider,
|
||||
RuntimeReleaseProvider
|
||||
}
|
||||
import org.enso.launcher.{FileSystem, Launcher, Logger}
|
||||
|
||||
import scala.util.{Failure, Success, Try}
|
||||
import scala.util.control.NonFatal
|
||||
|
||||
/**
|
||||
* Represents a runtime component.
|
||||
*
|
||||
* @param version version of the component
|
||||
* @param path path to the component
|
||||
*/
|
||||
case class Runtime(version: RuntimeVersion, path: Path) {
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
override def toString: String =
|
||||
s"GraalVM ${version.graal}-java${version.java}"
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents an engine component.
|
||||
*
|
||||
* @param version version of the component
|
||||
* @param path path to the component
|
||||
* @param manifest manifest of the engine release
|
||||
*/
|
||||
case class Engine(version: SemVer, path: Path, manifest: Manifest) {
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
override def toString: String =
|
||||
s"Enso Engine $version"
|
||||
}
|
||||
|
||||
/**
|
||||
* Manages runtime and engine components.
|
||||
*
|
||||
* Allows to find, list, install and uninstall components.
|
||||
*
|
||||
* @param cliOptions options from the CLI setting verbosity of the executed
|
||||
* actions
|
||||
* @param distributionManager the [[DistributionManager]] to use
|
||||
* @param engineReleaseProvider the provider of engine releases
|
||||
* @param runtimeReleaseProvider the provider of runtime releases
|
||||
*/
|
||||
class ComponentsManager(
|
||||
cliOptions: GlobalCLIOptions,
|
||||
distributionManager: DistributionManager,
|
||||
engineReleaseProvider: EngineReleaseProvider,
|
||||
runtimeReleaseProvider: RuntimeReleaseProvider
|
||||
) {
|
||||
private val showProgress = !cliOptions.hideProgress
|
||||
|
||||
/**
|
||||
* Tries to find runtime for the provided engine.
|
||||
*
|
||||
* Returns None if the runtime is missing.
|
||||
*/
|
||||
def findRuntime(engine: Engine): Option[Runtime] =
|
||||
findRuntime(engine.manifest.runtimeVersion)
|
||||
|
||||
/**
|
||||
* Finds an installed runtime with the given `version`.
|
||||
*
|
||||
* Returns None if that version is not installed.
|
||||
*/
|
||||
def findRuntime(version: RuntimeVersion): Option[Runtime] = {
|
||||
val name = runtimeNameForVersion(version)
|
||||
val path = distributionManager.paths.runtimes / name
|
||||
if (Files.exists(path)) {
|
||||
// TODO [RW] add a sanity check if runtime is in a working state - check
|
||||
// if it at least has the `java` executable, in #976 do the check and
|
||||
// throw an exception on failure, in #1052 offer to repair the broken
|
||||
// installation
|
||||
loadGraalRuntime(path)
|
||||
} else None
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the runtime needed for the given engine, trying to install it if
|
||||
* it is missing.
|
||||
*
|
||||
* @param engine the engine for which the runtime is requested
|
||||
* @param complain if set and the runtime is missing, prints a warning and
|
||||
* asks the user to install the missing runtime (unless
|
||||
* [[cliOptions.autoConfirm]] is set, in which case it
|
||||
* installs it without asking)
|
||||
*/
|
||||
def findOrInstallRuntime(
|
||||
engine: Engine,
|
||||
complain: Boolean = true
|
||||
): Runtime =
|
||||
findRuntime(engine) match {
|
||||
case Some(found) => found
|
||||
case None =>
|
||||
def complainAndAsk(): Boolean = {
|
||||
Logger.warn(
|
||||
s"Runtime ${engine.manifest.runtimeVersion} required for $engine " +
|
||||
s"is missing."
|
||||
)
|
||||
cliOptions.autoConfirm || CLIOutput.askConfirmation(
|
||||
"Do you want to install the missing runtime?",
|
||||
yesDefault = true
|
||||
)
|
||||
}
|
||||
if (!complain || complainAndAsk()) {
|
||||
installRuntime(engine.manifest.runtimeVersion)
|
||||
} else {
|
||||
throw ComponentMissingError(
|
||||
s"No runtime for engine $engine. Cannot continue."
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds an installed engine with the given `version` and reports any errors.
|
||||
*/
|
||||
def getEngine(version: SemVer): Try[Engine] = {
|
||||
val name = engineNameForVersion(version)
|
||||
val path = distributionManager.paths.engines / name
|
||||
if (Files.exists(path)) {
|
||||
// TODO [RW] right now we throw an exception, in the future (#1052) we
|
||||
// will try recovery
|
||||
loadEngine(path)
|
||||
} else Failure(ComponentMissingError(s"Engine $version is not installed."))
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds an engine with the given `version` or returns None if it is not
|
||||
* installed.
|
||||
*
|
||||
* Any other errors regarding loading the engine are thrown.
|
||||
*/
|
||||
def findEngine(version: SemVer): Option[Engine] =
|
||||
getEngine(version)
|
||||
.map(Some(_))
|
||||
.recoverWith {
|
||||
case _: ComponentMissingError => Success(None)
|
||||
case e: LauncherUpgradeRequiredError => Failure(e)
|
||||
case e: Exception =>
|
||||
Failure(
|
||||
UnrecognizedComponentError(
|
||||
s"The engine $version is already installed, but cannot be " +
|
||||
s"loaded due to $e. Until the launcher gets an auto-repair " +
|
||||
s"feature, please try running `enso uninstall engine $version` " +
|
||||
s"followed by `enso install engine $version`.",
|
||||
e
|
||||
)
|
||||
)
|
||||
}
|
||||
.get
|
||||
|
||||
/**
|
||||
* Returns the engine needed with the given version, trying to install it if
|
||||
* it is missing.
|
||||
*
|
||||
* @param version the requested engine version
|
||||
* @param complain if set and the engine is missing, prints a warning and
|
||||
* asks the user to install the missing engine (unless
|
||||
* [[cliOptions.autoConfirm]] is set, in which case it
|
||||
* installs it without asking)
|
||||
*/
|
||||
def findOrInstallEngine(version: SemVer, complain: Boolean = true): Engine =
|
||||
findEngine(version) match {
|
||||
case Some(found) => found
|
||||
case None =>
|
||||
def complainAndAsk(): Boolean = {
|
||||
Logger.warn(s"Engine $version is missing.")
|
||||
cliOptions.autoConfirm || CLIOutput.askConfirmation(
|
||||
"Do you want to install the missing engine?",
|
||||
yesDefault = true
|
||||
)
|
||||
}
|
||||
|
||||
if (!complain || complainAndAsk()) {
|
||||
installEngine(version)
|
||||
} else {
|
||||
throw ComponentMissingError(s"No engine $version. Cannot continue.")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds installed engines that use the given `runtime`.
|
||||
*/
|
||||
def findEnginesUsingRuntime(runtime: Runtime): Seq[Engine] =
|
||||
listInstalledEngines().filter(_.manifest.runtimeVersion == runtime.version)
|
||||
|
||||
/**
|
||||
* Lists all installed runtimes.
|
||||
*/
|
||||
def listInstalledRuntimes(): Seq[Runtime] =
|
||||
FileSystem
|
||||
.listDirectory(distributionManager.paths.runtimes)
|
||||
.flatMap(loadGraalRuntime)
|
||||
|
||||
/**
|
||||
* Lists all installed engines.
|
||||
* @return
|
||||
*/
|
||||
def listInstalledEngines(): Seq[Engine] = {
|
||||
def handleErrorsAsWarnings(path: Path, result: Try[Engine]): Seq[Engine] =
|
||||
result match {
|
||||
case Failure(exception) =>
|
||||
Logger.warn(
|
||||
s"An engine at $path has been skipped due to the " +
|
||||
s"following error: $exception"
|
||||
)
|
||||
Seq()
|
||||
case Success(value) => Seq(value)
|
||||
}
|
||||
FileSystem
|
||||
.listDirectory(distributionManager.paths.engines)
|
||||
.map(path => (path, loadEngine(path)))
|
||||
.flatMap((handleErrorsAsWarnings _).tupled)
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the latest released version of the engine, by asking the
|
||||
* [[engineReleaseProvider]].
|
||||
*/
|
||||
def fetchLatestEngineVersion(): SemVer =
|
||||
engineReleaseProvider.findLatest().get
|
||||
|
||||
/**
|
||||
* Uninstalls the engine with the provided `version` (if it was installed).
|
||||
*/
|
||||
def uninstallEngine(version: SemVer): Unit = {
|
||||
val engine = getEngine(version).getOrElse {
|
||||
Logger.warn(s"Enso Engine $version is not installed.")
|
||||
sys.exit(1)
|
||||
}
|
||||
|
||||
safelyRemoveComponent(engine.path)
|
||||
Logger.info(s"Uninstalled $engine.")
|
||||
cleanupRuntimes()
|
||||
}
|
||||
|
||||
/**
|
||||
* Installs the engine with the provided version.
|
||||
*
|
||||
* Used internally by [[findOrInstallEngine]]. Does not check if the engine
|
||||
* is already installed.
|
||||
*
|
||||
* The installation tries as much as possible to be robust - the downloaded
|
||||
* package is extracted to a temporary directory next to the `engines`
|
||||
* directory (to ensure that they are on the same filesystem) and is moved to
|
||||
* the actual directory after doing simple sanity checks.
|
||||
*/
|
||||
private def installEngine(version: SemVer): Engine = {
|
||||
val engineRelease = engineReleaseProvider.getRelease(version).get
|
||||
FileSystem.withTemporaryDirectory("enso-install") { directory =>
|
||||
Logger.debug(s"Downloading packages to $directory")
|
||||
val enginePackage = directory / engineRelease.packageFileName
|
||||
Logger.info(s"Downloading ${enginePackage.getFileName}")
|
||||
engineReleaseProvider
|
||||
.downloadPackage(engineRelease, enginePackage)
|
||||
.waitForResult(showProgress)
|
||||
.get
|
||||
|
||||
val engineDirectoryName =
|
||||
engineDirectoryNameForVersion(engineRelease.version)
|
||||
|
||||
Logger.info(s"Extracting engine")
|
||||
Archive
|
||||
.extractArchive(
|
||||
enginePackage,
|
||||
distributionManager.paths.temporaryDirectory,
|
||||
Some(engineDirectoryName)
|
||||
)
|
||||
.waitForResult(showProgress)
|
||||
.get
|
||||
|
||||
val engineTemporaryPath =
|
||||
distributionManager.paths.temporaryDirectory / engineDirectoryName
|
||||
def undoTemporaryEngine(): Unit = {
|
||||
if (Files.exists(engineTemporaryPath)) {
|
||||
FileSystem.removeDirectory(engineTemporaryPath)
|
||||
}
|
||||
}
|
||||
|
||||
val temporaryEngine = loadEngine(engineTemporaryPath).getOrElse {
|
||||
undoTemporaryEngine()
|
||||
throw InstallationError(
|
||||
"Cannot load downloaded engine. Installation reverted."
|
||||
)
|
||||
}
|
||||
|
||||
try {
|
||||
if (temporaryEngine.manifest != engineRelease.manifest) {
|
||||
undoTemporaryEngine()
|
||||
throw InstallationError(
|
||||
"Manifest of installed engine does not match the published " +
|
||||
"manifest. This may lead to version inconsistencies; the package " +
|
||||
"may possibly be corrupted. Reverting installation."
|
||||
)
|
||||
}
|
||||
|
||||
findOrInstallRuntime(temporaryEngine, complain = false)
|
||||
|
||||
val enginePath = distributionManager.paths.engines / engineDirectoryName
|
||||
FileSystem.atomicMove(engineTemporaryPath, enginePath)
|
||||
val engine = getEngine(version).getOrElse {
|
||||
Logger.error(
|
||||
"fatal: Could not load the installed engine." +
|
||||
"Reverting the installation."
|
||||
)
|
||||
FileSystem.removeDirectory(enginePath)
|
||||
cleanupRuntimes()
|
||||
throw InstallationError(
|
||||
"fatal: Could not load the installed engine"
|
||||
)
|
||||
}
|
||||
|
||||
Logger.info(s"Installed $engine.")
|
||||
engine
|
||||
} catch {
|
||||
case e: Exception =>
|
||||
undoTemporaryEngine()
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns name of the directory containing the engine of that version.
|
||||
*/
|
||||
private def engineNameForVersion(version: SemVer): String =
|
||||
version.toString
|
||||
|
||||
/**
|
||||
* Returns name of the directory containing the runtime of that version.
|
||||
*/
|
||||
private def runtimeNameForVersion(version: RuntimeVersion): String =
|
||||
s"graalvm-ce-java${version.java}-${version.graal}"
|
||||
|
||||
/**
|
||||
* Loads the GraalVM runtime definition.
|
||||
*
|
||||
* Returns None on failure.
|
||||
*/
|
||||
private def loadGraalRuntime(path: Path): Option[Runtime] = {
|
||||
val name = path.getFileName.toString
|
||||
for {
|
||||
version <- parseGraalRuntimeVersionString(name)
|
||||
} yield Runtime(version, path)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the runtime version from its name.
|
||||
*/
|
||||
private def parseGraalRuntimeVersionString(
|
||||
name: String
|
||||
): Option[RuntimeVersion] = {
|
||||
val regex = """graalvm-ce-java(\d+)-(.+)""".r
|
||||
name match {
|
||||
case regex(javaVersionString, graalVersionString) =>
|
||||
SemVer(graalVersionString) match {
|
||||
case Some(graalVersion) =>
|
||||
Some(RuntimeVersion(graalVersion, javaVersionString))
|
||||
case None =>
|
||||
Logger.warn(
|
||||
s"Invalid runtime version string `$graalVersionString`."
|
||||
)
|
||||
None
|
||||
}
|
||||
case _ =>
|
||||
Logger.warn(
|
||||
s"Unrecognized runtime name `$name`."
|
||||
)
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the engine definition.
|
||||
*
|
||||
* Returns None on failure.
|
||||
*/
|
||||
private def loadEngine(path: Path): Try[Engine] =
|
||||
for {
|
||||
version <- parseEngineVersion(path)
|
||||
manifest <- loadAndCheckEngineManifest(path)
|
||||
} yield Engine(version, path, manifest)
|
||||
|
||||
/**
|
||||
* Gets the engine version from its path.
|
||||
*/
|
||||
private def parseEngineVersion(path: Path): Try[SemVer] = {
|
||||
val name = path.getFileName.toString
|
||||
SemVer(name)
|
||||
.toRight(
|
||||
UnrecognizedComponentError(
|
||||
s"Invalid engine component version `$name`."
|
||||
)
|
||||
)
|
||||
.toTry
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the engine manifest, checking if that release is compatible with the
|
||||
* currently running launcher.
|
||||
*/
|
||||
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))
|
||||
} else Success(manifest)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Installs the runtime with the provided version.
|
||||
*
|
||||
* Used internally by [[findOrInstallRuntime]]. Does not check if the runtime
|
||||
* is already installed.
|
||||
*
|
||||
* The installation tries as much as possible to be robust - the downloaded
|
||||
* package is extracted to a temporary directory next to the `runtimes`
|
||||
* directory (to ensure that they are on the same filesystem) and is moved to
|
||||
* the actual directory after doing simple sanity checks.
|
||||
*/
|
||||
private def installRuntime(runtimeVersion: RuntimeVersion): Runtime =
|
||||
FileSystem.withTemporaryDirectory("enso-install-runtime") { directory =>
|
||||
val runtimePackage =
|
||||
directory / runtimeReleaseProvider.packageFileName(runtimeVersion)
|
||||
Logger.info(s"Downloading ${runtimePackage.getFileName}")
|
||||
runtimeReleaseProvider
|
||||
.downloadPackage(runtimeVersion, runtimePackage)
|
||||
.waitForResult(showProgress)
|
||||
.get
|
||||
|
||||
val runtimeDirectoryName = graalDirectoryForVersion(runtimeVersion)
|
||||
|
||||
Logger.info(s"Extracting runtime")
|
||||
Archive
|
||||
.extractArchive(
|
||||
runtimePackage,
|
||||
distributionManager.paths.temporaryDirectory,
|
||||
Some(runtimeDirectoryName)
|
||||
)
|
||||
.waitForResult(showProgress)
|
||||
.get
|
||||
|
||||
val runtimeTemporaryPath =
|
||||
distributionManager.paths.temporaryDirectory / runtimeDirectoryName
|
||||
|
||||
def undoTemporaryRuntime(): Unit = {
|
||||
if (Files.exists(runtimeTemporaryPath)) {
|
||||
FileSystem.removeDirectory(runtimeTemporaryPath)
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
val temporaryRuntime = loadGraalRuntime(runtimeTemporaryPath)
|
||||
if (temporaryRuntime.isEmpty) {
|
||||
throw InstallationError(
|
||||
"Cannot load the installed runtime. The package may have been " +
|
||||
"corrupted. Reverting installation."
|
||||
)
|
||||
}
|
||||
|
||||
val runtimePath =
|
||||
distributionManager.paths.runtimes / runtimeDirectoryName
|
||||
FileSystem.atomicMove(runtimeTemporaryPath, runtimePath)
|
||||
val runtime = loadGraalRuntime(runtimePath).getOrElse {
|
||||
FileSystem.removeDirectory(runtimePath)
|
||||
throw InstallationError(
|
||||
"fatal: Cannot load the installed runtime."
|
||||
)
|
||||
}
|
||||
|
||||
Logger.info(s"Installed $runtime.")
|
||||
runtime
|
||||
} catch {
|
||||
case NonFatal(e) =>
|
||||
undoTemporaryRuntime()
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
private def engineDirectoryNameForVersion(version: SemVer): Path =
|
||||
Path.of(version.toString())
|
||||
|
||||
private def graalDirectoryForVersion(version: RuntimeVersion): Path =
|
||||
Path.of(s"graalvm-ce-java${version.java}-${version.graal}")
|
||||
|
||||
/**
|
||||
* Removes runtimes that are not used by any installed engines.
|
||||
*/
|
||||
def cleanupRuntimes(): Unit = {
|
||||
for (runtime <- listInstalledRuntimes()) {
|
||||
if (findEnginesUsingRuntime(runtime).isEmpty) {
|
||||
Logger.info(
|
||||
s"Removing $runtime, because it is not used by any installed Enso " +
|
||||
s"versions."
|
||||
)
|
||||
safelyRemoveComponent(runtime.path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to remove a component in a safe way.
|
||||
*
|
||||
* The component is moved (hopefully atomically) to temporary directory next
|
||||
* to the actual components directories and only then it is removed from
|
||||
* there. As the move should be executed as a single operation, there is no
|
||||
* risk that the system may leave the component corrupted if the deletion
|
||||
* were abruptly terminated. The component may be corrupted while being
|
||||
* removed, but it will already be in the temporary directory, so it will be
|
||||
* unreachable. The temporary directory is cleaned when doing
|
||||
* installation-related operations.
|
||||
*/
|
||||
private def safelyRemoveComponent(path: Path): Unit = {
|
||||
val temporaryPath =
|
||||
distributionManager.paths.temporaryDirectory / path.getFileName
|
||||
FileSystem.atomicMove(path, temporaryPath)
|
||||
FileSystem.removeDirectory(temporaryPath)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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,
|
||||
DistributionManager,
|
||||
EngineReleaseProvider,
|
||||
GraalCEReleaseProvider
|
||||
)
|
@ -0,0 +1,122 @@
|
||||
package org.enso.launcher.components
|
||||
|
||||
import java.io.FileReader
|
||||
import java.nio.file.Path
|
||||
|
||||
import io.circe.{yaml, Decoder, DecodingFailure}
|
||||
import nl.gn0s1s.bump.SemVer
|
||||
|
||||
import scala.util.{Failure, Try, Using}
|
||||
|
||||
/**
|
||||
* Version information identifying the runtime that can be used with an engine
|
||||
* release.
|
||||
*
|
||||
* @param graal version of the GraalVM
|
||||
* @param java Java version of the GraalVM flavour that should be used
|
||||
*/
|
||||
case class RuntimeVersion(graal: SemVer, java: String) {
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
override def toString: String = s"GraalVM $graal Java $java"
|
||||
}
|
||||
|
||||
/**
|
||||
* Contains release metadata read from the manifest file that is attached to
|
||||
* each release.
|
||||
*
|
||||
* @param minimumLauncherVersion The minimum required version of the launcher
|
||||
* that can be used to run this engine release.
|
||||
* Earlier launcher versions should not be able
|
||||
* to download this release, but print a message
|
||||
* that the launcher needs an upgrade.
|
||||
* @param graalVMVersion the version of the GraalVM runtime that has to be
|
||||
* used with this engine
|
||||
* @param graalJavaVersion the java version of that GraalVM runtime
|
||||
*/
|
||||
case class Manifest(
|
||||
minimumLauncherVersion: SemVer,
|
||||
graalVMVersion: SemVer,
|
||||
graalJavaVersion: String
|
||||
) {
|
||||
|
||||
/**
|
||||
* Returns a [[RuntimeVersion]] which encapsulates all version information
|
||||
* needed to find the runtime required for this release.
|
||||
*/
|
||||
def runtimeVersion: RuntimeVersion =
|
||||
RuntimeVersion(graalVMVersion, graalJavaVersion)
|
||||
}
|
||||
|
||||
object Manifest {
|
||||
|
||||
/**
|
||||
* Defines the name under which the manifest is included in the releases.
|
||||
*/
|
||||
val DEFAULT_MANIFEST_NAME = "manifest.yaml"
|
||||
|
||||
/**
|
||||
* Tries to load the manifest at the given path.
|
||||
*
|
||||
* Returns None if the manifest could not be opened or could not be parsed.
|
||||
*/
|
||||
def load(path: Path): Try[Manifest] =
|
||||
Using(new FileReader(path.toFile)) { reader =>
|
||||
yaml.parser
|
||||
.parse(reader)
|
||||
.flatMap(_.as[Manifest])
|
||||
.toTry
|
||||
.recoverWith { error => Failure(ManifestLoadingError(error)) }
|
||||
}.flatten
|
||||
|
||||
/**
|
||||
* Parses the manifest from a string containing a YAML definition.
|
||||
*
|
||||
* Returns None if the definition cannot be parsed.
|
||||
*/
|
||||
def fromYaml(yamlString: String): Try[Manifest] = {
|
||||
yaml.parser
|
||||
.parse(yamlString)
|
||||
.flatMap(_.as[Manifest])
|
||||
.toTry
|
||||
.recoverWith { error => Failure(ManifestLoadingError(error)) }
|
||||
}
|
||||
|
||||
case class ManifestLoadingError(cause: Throwable)
|
||||
extends RuntimeException(s"Could not load the manifest: $cause", cause)
|
||||
|
||||
private object Fields {
|
||||
val minimumLauncherVersion = "minimum-launcher-version"
|
||||
val graalVMVersion = "graal-vm-version"
|
||||
val graalJavaVersion = "graal-java-version"
|
||||
}
|
||||
|
||||
implicit private val semverDecoder: Decoder[SemVer] = { json =>
|
||||
for {
|
||||
string <- json.as[String]
|
||||
version <- SemVer(string).toRight(
|
||||
DecodingFailure(
|
||||
s"`$string` is not a valid semver version.",
|
||||
json.history
|
||||
)
|
||||
)
|
||||
} yield version
|
||||
}
|
||||
|
||||
implicit private val decoder: Decoder[Manifest] = { json =>
|
||||
for {
|
||||
minimumLauncherVersion <- json.get[SemVer](Fields.minimumLauncherVersion)
|
||||
graalVMVersion <- json.get[SemVer](Fields.graalVMVersion)
|
||||
graalJavaVersion <-
|
||||
json
|
||||
.get[String](Fields.graalJavaVersion)
|
||||
.orElse(json.get[Int](Fields.graalJavaVersion).map(_.toString))
|
||||
} yield Manifest(
|
||||
minimumLauncherVersion = minimumLauncherVersion,
|
||||
graalVMVersion = graalVMVersion,
|
||||
graalJavaVersion = graalJavaVersion
|
||||
)
|
||||
}
|
||||
}
|
@ -0,0 +1,175 @@
|
||||
package org.enso.launcher.http
|
||||
|
||||
import java.io.FileOutputStream
|
||||
import java.nio.charset.{Charset, StandardCharsets}
|
||||
import java.nio.file.Path
|
||||
|
||||
import org.apache.commons.io.IOUtils
|
||||
import org.apache.http.client.config.{CookieSpecs, RequestConfig}
|
||||
import org.apache.http.client.methods.HttpUriRequest
|
||||
import org.apache.http.impl.client.HttpClients
|
||||
import org.apache.http.{Header, HttpResponse}
|
||||
import org.enso.cli.{TaskProgress, TaskProgressImplementation}
|
||||
import org.enso.launcher.Logger
|
||||
import org.enso.launcher.internal.{ProgressInputStream, ReadProgress}
|
||||
|
||||
import scala.util.control.NonFatal
|
||||
import scala.util.{Failure, Success, Using}
|
||||
|
||||
/**
|
||||
* Contains the response contents as a string alongside with the headers
|
||||
* included in the response.
|
||||
*
|
||||
* @param content the response decoded as a string
|
||||
* @param headers sequence of headers included in the response
|
||||
*/
|
||||
case class APIResponse(content: String, headers: Seq[Header])
|
||||
|
||||
/**
|
||||
* Contains utility functions for fetching data using the HTTP(S) protocol.
|
||||
*/
|
||||
object HTTPDownload {
|
||||
|
||||
/**
|
||||
* Fetches the `request` and tries to decode is as a [[String]].
|
||||
*
|
||||
* The request is executed in a separate thread. A [[TaskProgress]] instance
|
||||
* is returned immediately which can be used to track progress of the
|
||||
* download. The result contains the decoded response and included headers.
|
||||
*
|
||||
* @param request the request to send
|
||||
* @param sizeHint an optional hint indicating the expected size of the
|
||||
* response. It is used if the response does not include
|
||||
* explicit Content-Length header.
|
||||
* @param encoding the encoding used to decode the response content into a
|
||||
* string. By default, UTF-8 is used.
|
||||
* @return a [[TaskProgress]] that tracks progress of the download and can
|
||||
* be used to get the final result
|
||||
*/
|
||||
def fetchString(
|
||||
request: HttpUriRequest,
|
||||
sizeHint: Option[Long] = None,
|
||||
encoding: Charset = StandardCharsets.UTF_8
|
||||
): TaskProgress[APIResponse] = {
|
||||
Logger.debug(s"Fetching ${request.getURI.toASCIIString}")
|
||||
runRequest(request, asStringResponseHandler(sizeHint, encoding))
|
||||
}
|
||||
|
||||
/**
|
||||
* Downloads the `request` and saves the response in the file pointed by the
|
||||
* `destination`.
|
||||
*
|
||||
* The request is executed in a separate thread. A [[TaskProgress]] instance
|
||||
* is returned immediately which can be used to track progress of the
|
||||
* download. The result is the same path as `destination`. It is available
|
||||
* only when the download has been completed successfully.
|
||||
*
|
||||
* @param request the request to send
|
||||
* @param sizeHint an optional hint indicating the expected size of the
|
||||
* response. It is used if the response does not include
|
||||
* explicit Content-Length header.
|
||||
* @return a [[TaskProgress]] that tracks progress of the download and can
|
||||
* be used to wait for the completion of the download.
|
||||
*/
|
||||
def download(
|
||||
request: HttpUriRequest,
|
||||
destination: Path,
|
||||
sizeHint: Option[Long] = None
|
||||
): TaskProgress[Path] = {
|
||||
Logger.debug(s"Downloading ${request.getURI.toASCIIString} to $destination")
|
||||
runRequest(request, asFileHandler(destination, sizeHint))
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new thread that will send the provided request and returns a
|
||||
* [[TaskProgress]] monitoring progress of that request.
|
||||
*
|
||||
* @param request the request to send
|
||||
* @param handler a handler that processes the the received response to
|
||||
* produce some result. The second argument of the handler is
|
||||
* a callback that should be used by the handler to report
|
||||
* progress.
|
||||
* @tparam A type of the result generated on success
|
||||
* @return [[TaskProgress]] that monitors request progress and will return
|
||||
* the response returned by the `handler`
|
||||
*/
|
||||
private def runRequest[A](
|
||||
request: HttpUriRequest,
|
||||
handler: (HttpResponse, ReadProgress => Unit) => A
|
||||
): TaskProgress[A] = {
|
||||
val task = new TaskProgressImplementation[A]
|
||||
def update(progress: ReadProgress): Unit = {
|
||||
task.reportProgress(progress.alreadyRead(), progress.total())
|
||||
}
|
||||
|
||||
def run(): Unit = {
|
||||
try {
|
||||
val client = buildClient()
|
||||
Using(client.execute(request)) { response =>
|
||||
val result = handler(response, update)
|
||||
task.setComplete(Success(result))
|
||||
}
|
||||
} catch {
|
||||
case NonFatal(e) => task.setComplete(Failure(e))
|
||||
}
|
||||
}
|
||||
|
||||
val thread = new Thread(() => run(), "HTTP-Runner")
|
||||
thread.start()
|
||||
task
|
||||
}
|
||||
|
||||
private def buildClient() =
|
||||
HttpClients
|
||||
.custom()
|
||||
.setDefaultRequestConfig(
|
||||
RequestConfig.custom().setCookieSpec(CookieSpecs.STANDARD).build()
|
||||
)
|
||||
.build()
|
||||
|
||||
/**
|
||||
* Creates a handler that tries to decode the result content as a [[String]].
|
||||
*/
|
||||
private def asStringResponseHandler(
|
||||
sizeHint: Option[Long],
|
||||
charset: Charset
|
||||
)(response: HttpResponse, update: ReadProgress => Unit): APIResponse =
|
||||
Using(streamOfResponse(response, update, sizeHint)) { in =>
|
||||
val bytes = in.readAllBytes()
|
||||
val content = new String(bytes, charset)
|
||||
APIResponse(content, response.getAllHeaders.toIndexedSeq)
|
||||
}.get
|
||||
|
||||
/**
|
||||
* Creates a handler that tries to save the result content into a file.
|
||||
*/
|
||||
private def asFileHandler(
|
||||
path: Path,
|
||||
sizeHint: Option[Long]
|
||||
)(response: HttpResponse, update: ReadProgress => Unit): Path =
|
||||
Using(streamOfResponse(response, update, sizeHint)) { in =>
|
||||
Using(new FileOutputStream(path.toFile)) { out =>
|
||||
IOUtils.copy(in, out)
|
||||
path
|
||||
}.get
|
||||
}.get
|
||||
|
||||
/**
|
||||
* Returns a progress-monitored stream that can be used to read the response
|
||||
* content.
|
||||
*/
|
||||
private def streamOfResponse(
|
||||
response: HttpResponse,
|
||||
update: ReadProgress => Unit,
|
||||
sizeHint: Option[Long]
|
||||
): ProgressInputStream = {
|
||||
val entity = response.getEntity
|
||||
val size = {
|
||||
val len = entity.getContentLength
|
||||
if (len < 0) None
|
||||
else Some(len)
|
||||
}
|
||||
|
||||
new ProgressInputStream(entity.getContent, size.orElse(sizeHint), update)
|
||||
}
|
||||
}
|
@ -0,0 +1,55 @@
|
||||
package org.enso.launcher.http
|
||||
|
||||
import java.net.URI
|
||||
|
||||
import org.apache.http.client.methods.{HttpUriRequest, RequestBuilder}
|
||||
|
||||
/**
|
||||
* A simple immutable builder for HTTP requests.
|
||||
*
|
||||
* It contains very limited functionality that is needed by the APIs used in
|
||||
* the launcher. It can be easily extended if necessary.
|
||||
*/
|
||||
case class HTTPRequestBuilder private (
|
||||
uri: URI,
|
||||
headers: Vector[(String, String)]
|
||||
) {
|
||||
|
||||
/**
|
||||
* Builds a GET request with the specified settings.
|
||||
*/
|
||||
def GET: HttpUriRequest = build(RequestBuilder.get())
|
||||
|
||||
/**
|
||||
* Adds an additional header that will be included in the request.
|
||||
*
|
||||
* @param name name of the header
|
||||
* @param value the header value
|
||||
*/
|
||||
def addHeader(name: String, value: String): HTTPRequestBuilder =
|
||||
copy(headers = headers.appended((name, value)))
|
||||
|
||||
private def build(requestBuilder: RequestBuilder): HttpUriRequest = {
|
||||
val withUri = requestBuilder.setUri(uri)
|
||||
val withHeaders = headers.foldLeft(withUri)((builder, header) =>
|
||||
builder.addHeader(header._1, header._2)
|
||||
)
|
||||
withHeaders.build()
|
||||
}
|
||||
}
|
||||
|
||||
object HTTPRequestBuilder {
|
||||
|
||||
/**
|
||||
* Creates a request builder that will send the request for the given URI.
|
||||
*/
|
||||
def fromURI(uri: URI): HTTPRequestBuilder =
|
||||
new HTTPRequestBuilder(uri, Vector.empty)
|
||||
|
||||
/**
|
||||
* Tries to parse the URI provided as a [[String]] and returns a request
|
||||
* builder that will send the request to the given `uri`.
|
||||
*/
|
||||
def fromURIString(uri: String): HTTPRequestBuilder =
|
||||
fromURI(new URI(uri))
|
||||
}
|
@ -0,0 +1,79 @@
|
||||
package org.enso.launcher.http
|
||||
|
||||
import java.net.URI
|
||||
|
||||
import org.apache.http.client.utils.{URIBuilder => ApacheURIBuilder}
|
||||
|
||||
/**
|
||||
* A simple immutable builder for URIs based on URLs.
|
||||
*
|
||||
* It contains very limited functionality that is needed by the APIs used in
|
||||
* the launcher. It can be easily extended if necessary.
|
||||
*
|
||||
* As all APIs we use support HTTPS, it does not allow to create a non-HTTPS
|
||||
* URL.
|
||||
*/
|
||||
case class URIBuilder private (
|
||||
host: String,
|
||||
segments: Vector[String],
|
||||
queries: Vector[(String, String)]
|
||||
) {
|
||||
|
||||
/**
|
||||
* Resolve a segment over the path in the URI.
|
||||
*
|
||||
* For example adding `bar` to `http://example.com/foo` will result in
|
||||
* `http://example.com/foo/bar`.
|
||||
*/
|
||||
def addPathSegment(segment: String): URIBuilder =
|
||||
copy(segments = segments.appended(segment))
|
||||
|
||||
/**
|
||||
* Add a query parameter to the URI.
|
||||
*
|
||||
* The query is appended at the end.
|
||||
*/
|
||||
def addQuery(key: String, value: String): URIBuilder =
|
||||
copy(queries = queries.appended((key, value)))
|
||||
|
||||
/**
|
||||
* Build the URI represented by this builder.
|
||||
*/
|
||||
def build(): URI = {
|
||||
val base = (new ApacheURIBuilder)
|
||||
.setScheme("https")
|
||||
.setHost(host)
|
||||
.setPathSegments(segments: _*)
|
||||
val withQueries = queries.foldLeft(base)((builder, query) =>
|
||||
builder.addParameter(query._1, query._2)
|
||||
)
|
||||
withQueries.build()
|
||||
}
|
||||
}
|
||||
|
||||
object URIBuilder {
|
||||
|
||||
/**
|
||||
* Create a builder basing from a hostname.
|
||||
*
|
||||
* A builder created by `fromHost("example.com")` represents
|
||||
* `https://example.com/`.
|
||||
*/
|
||||
def fromHost(host: String): URIBuilder =
|
||||
new URIBuilder(
|
||||
host = host,
|
||||
segments = Vector.empty,
|
||||
queries = Vector.empty
|
||||
)
|
||||
|
||||
/**
|
||||
* A simple DSL for the URIBuilder.
|
||||
*/
|
||||
implicit class URIBuilderSyntax(builder: URIBuilder) {
|
||||
def /(part: String): URIBuilder =
|
||||
builder.addPathSegment(part)
|
||||
|
||||
def ?(query: (String, String)): URIBuilder =
|
||||
builder.addQuery(query._1, query._2)
|
||||
}
|
||||
}
|
@ -4,14 +4,8 @@ import java.nio.file.Files
|
||||
|
||||
import org.enso.cli.CLIOutput
|
||||
import org.enso.launcher.FileSystem.PathSyntax
|
||||
import org.enso.launcher.internal.OS
|
||||
import org.enso.launcher.internal.installation.DistributionManager
|
||||
import org.enso.launcher.{
|
||||
FileSystem,
|
||||
GlobalConfigurationManager,
|
||||
InternalOpts,
|
||||
Logger
|
||||
}
|
||||
import org.enso.launcher.cli.InternalOpts
|
||||
import org.enso.launcher.{FileSystem, GlobalConfigurationManager, Logger, OS}
|
||||
|
||||
import scala.util.control.NonFatal
|
||||
|
||||
@ -23,6 +17,9 @@ import scala.util.control.NonFatal
|
||||
* location
|
||||
* @param autoConfirm if set to true, the installer will use defaults instead
|
||||
* of asking questions
|
||||
* @param removeOldLauncher if `autoConfirm` is set to true, specifies whether
|
||||
* the old launcher should be removed after successful
|
||||
* installation
|
||||
* @param bundleActionOption defines how bundled components are added, if
|
||||
* [[autoConfirm]] is set, defaults to a move,
|
||||
* otherwise explicitly asks the user
|
||||
@ -30,6 +27,7 @@ import scala.util.control.NonFatal
|
||||
class DistributionInstaller(
|
||||
manager: DistributionManager,
|
||||
autoConfirm: Boolean,
|
||||
removeOldLauncher: Boolean,
|
||||
bundleActionOption: Option[DistributionInstaller.BundleAction]
|
||||
) {
|
||||
final private val installed = manager.LocallyInstalledDirectories
|
||||
@ -335,7 +333,11 @@ class DistributionInstaller(
|
||||
)
|
||||
|
||||
if (installedLauncherPath != currentLauncherPath) {
|
||||
if (autoConfirm || askForRemoval()) {
|
||||
def shouldRemove(): Boolean =
|
||||
if (autoConfirm) removeOldLauncher
|
||||
else askForRemoval()
|
||||
|
||||
if (shouldRemove()) {
|
||||
if (OS.isWindows) {
|
||||
InternalOpts
|
||||
.runWithNewLauncher(installedLauncherPath)
|
||||
@ -371,22 +373,83 @@ object DistributionInstaller {
|
||||
def delete: Boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* The bundle action that will copy the bundles and keep the ones in the
|
||||
* original location too.
|
||||
*/
|
||||
case object CopyBundles extends BundleAction {
|
||||
override def key: String = "c"
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
override def key: String = "c"
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
override def description: String = "copy bundles"
|
||||
def copy: Boolean = true
|
||||
def delete: Boolean = false
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
def copy: Boolean = true
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
def delete: Boolean = false
|
||||
}
|
||||
|
||||
/**
|
||||
* The bundle action that will copy the bundles and remove the ones at the
|
||||
* original location on success.
|
||||
*/
|
||||
case object MoveBundles extends BundleAction {
|
||||
override def key: String = "m"
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
override def key: String = "m"
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
override def description: String = "move bundles"
|
||||
def copy: Boolean = true
|
||||
def delete: Boolean = true
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
def copy: Boolean = true
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
def delete: Boolean = true
|
||||
}
|
||||
|
||||
/**
|
||||
* The bundle action that ignores the bundles.
|
||||
*/
|
||||
case object IgnoreBundles extends BundleAction {
|
||||
override def key: String = "i"
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
override def key: String = "i"
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
override def description: String = "ignore bundles"
|
||||
def copy: Boolean = false
|
||||
def delete: Boolean = false
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
def copy: Boolean = false
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
def delete: Boolean = false
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,271 @@
|
||||
package org.enso.launcher.installation
|
||||
|
||||
import org.enso.launcher.Environment
|
||||
import org.enso.launcher.internal.installation.DistributionManager
|
||||
import java.nio.file.{Files, Path}
|
||||
|
||||
import org.enso.launcher.FileSystem.PathSyntax
|
||||
import org.enso.launcher.{Environment, FileSystem, Logger, OS}
|
||||
|
||||
import scala.util.Try
|
||||
|
||||
/**
|
||||
* Gathers filesystem paths used by the launcher.
|
||||
*
|
||||
* @param dataRoot the root of the data directory; for a portable distribution
|
||||
* this is the root of the distribution, for a locally
|
||||
* installed distribution, it corresponds to `ENSO_DATA_DIR`
|
||||
* @param runtimes location of runtimes, corresponding to `runtime` directory
|
||||
* @param engines location of engine versions, corresponding to `dist`
|
||||
* directory
|
||||
* @param config location of configuration
|
||||
* @param tmp a directory for storing temporary files that is located on the
|
||||
* same filesystem as `runtimes` and `engines`, used during
|
||||
* installation to decrease the possibility of getting a broken
|
||||
* installation if the installation process has been abruptly
|
||||
* terminated. The directory is created on demand (when its path is
|
||||
* requested for the first time) and is removed if the application
|
||||
* exits normally (as long as it is empty, but normal termination of
|
||||
* the installation process should ensure that).
|
||||
*/
|
||||
case class DistributionPaths(
|
||||
dataRoot: Path,
|
||||
runtimes: Path,
|
||||
engines: Path,
|
||||
config: Path,
|
||||
private val tmp: Path
|
||||
) {
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
override def toString: String =
|
||||
s"""DistributionPaths(
|
||||
| dataRoot = $dataRoot,
|
||||
| runtimes = $runtimes,
|
||||
| engines = $engines,
|
||||
| config = $config,
|
||||
| tmp = $tmp
|
||||
|)""".stripMargin
|
||||
|
||||
lazy val temporaryDirectory: Path = {
|
||||
runCleanup()
|
||||
tmp
|
||||
}
|
||||
|
||||
private def runCleanup(): Unit = {
|
||||
if (Files.exists(tmp)) {
|
||||
if (!FileSystem.isDirectoryEmpty(tmp)) {
|
||||
Logger.info("Cleaning up temporary files from a previous installation.")
|
||||
}
|
||||
FileSystem.removeDirectory(tmp)
|
||||
Files.createDirectories(tmp)
|
||||
FileSystem.removeEmptyDirectoryOnExit(tmp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A helper class that detects if a portable or installed distribution is run
|
||||
* and encapsulates management of paths to components of the distribution.
|
||||
*/
|
||||
class DistributionManager(val env: Environment) {
|
||||
|
||||
/**
|
||||
* Specifies whether the launcher has been run as a portable distribution or
|
||||
* it is a locally installed distribution.
|
||||
*/
|
||||
lazy val isRunningPortable: Boolean = {
|
||||
val portable = detectPortable()
|
||||
Logger.debug(s"Launcher portable mode = $portable")
|
||||
if (portable && LocallyInstalledDirectories.installedDistributionExists) {
|
||||
val installedRoot = LocallyInstalledDirectories.dataDirectory
|
||||
val installedBinary = LocallyInstalledDirectories.binaryExecutable
|
||||
|
||||
Logger.debug(
|
||||
s"The launcher is run in portable mode, but an installed distribution" +
|
||||
s" is available at $installedRoot."
|
||||
)
|
||||
|
||||
if (Files.exists(installedBinary)) {
|
||||
if (installedBinary == env.getPathToRunningExecutable) {
|
||||
Logger.debug(
|
||||
"That distribution seems to be corresponding to this launcher " +
|
||||
"executable, that is running in portable mode."
|
||||
)
|
||||
} else {
|
||||
Logger.debug(
|
||||
s"However, that installed distribution most likely uses another " +
|
||||
s"launcher executable, located at $installedBinary."
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
portable
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines paths that should be used by the launcher.
|
||||
*/
|
||||
lazy val paths: DistributionPaths = {
|
||||
val paths = detectPaths()
|
||||
Logger.debug(s"Detected paths are: $paths")
|
||||
|
||||
paths
|
||||
}
|
||||
|
||||
private val PORTABLE_MARK_FILENAME = ".enso.portable"
|
||||
val ENGINES_DIRECTORY = "dist"
|
||||
val RUNTIMES_DIRECTORY = "runtime"
|
||||
val CONFIG_DIRECTORY = "config"
|
||||
val BIN_DIRECTORY = "bin"
|
||||
private val TMP_DIRECTORY = "tmp"
|
||||
|
||||
private def detectPortable(): Boolean = Files.exists(portableMarkFilePath)
|
||||
private def possiblePortableRoot: Path =
|
||||
env.getPathToRunningExecutable.getParent.getParent
|
||||
|
||||
private def portableMarkFilePath: Path =
|
||||
possiblePortableRoot / PORTABLE_MARK_FILENAME
|
||||
|
||||
private def detectPaths(): DistributionPaths =
|
||||
if (isRunningPortable) {
|
||||
val root = env.getPathToRunningExecutable.getParent.getParent
|
||||
DistributionPaths(
|
||||
dataRoot = root,
|
||||
runtimes = root / RUNTIMES_DIRECTORY,
|
||||
engines = root / ENGINES_DIRECTORY,
|
||||
config = root / CONFIG_DIRECTORY,
|
||||
tmp = root / TMP_DIRECTORY
|
||||
)
|
||||
} else {
|
||||
val dataRoot = LocallyInstalledDirectories.dataDirectory
|
||||
val configRoot = LocallyInstalledDirectories.configDirectory
|
||||
DistributionPaths(
|
||||
dataRoot = dataRoot,
|
||||
runtimes = dataRoot / RUNTIMES_DIRECTORY,
|
||||
engines = dataRoot / ENGINES_DIRECTORY,
|
||||
config = configRoot,
|
||||
tmp = dataRoot / TMP_DIRECTORY
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* A helper for managing directories of the non-portable installation.
|
||||
*
|
||||
* It returns paths of the non-portable installation even if the launcher is
|
||||
* running in portable mode, so that this helper can be used by the installer
|
||||
* to determine destination for installed files.
|
||||
*/
|
||||
object LocallyInstalledDirectories {
|
||||
val ENSO_DATA_DIRECTORY = "ENSO_DATA_DIRECTORY"
|
||||
val ENSO_CONFIG_DIRECTORY = "ENSO_CONFIG_DIRECTORY"
|
||||
val ENSO_BIN_DIRECTORY = "ENSO_BIN_DIRECTORY"
|
||||
|
||||
private val XDG_DATA_DIRECTORY = "XDG_DATA_DIRECTORY"
|
||||
private val XDG_CONFIG_DIRECTORY = "XDG_CONFIG_DIRECTORY"
|
||||
private val XDG_BIN_DIRECTORY = "XDG_BIN_DIRECTORY"
|
||||
|
||||
private val LINUX_ENSO_DIRECTORY = "enso"
|
||||
private val MACOS_ENSO_DIRECTORY = "org.enso"
|
||||
private val WINDOWS_ENSO_DIRECTORY = "enso"
|
||||
|
||||
/**
|
||||
* Data directory for an installed distribution.
|
||||
*/
|
||||
def dataDirectory: Path =
|
||||
env
|
||||
.getEnvPath(ENSO_DATA_DIRECTORY)
|
||||
.getOrElse {
|
||||
OS.operatingSystem match {
|
||||
case OS.Linux =>
|
||||
env
|
||||
.getEnvPath(XDG_DATA_DIRECTORY)
|
||||
.map(_ / LINUX_ENSO_DIRECTORY)
|
||||
.getOrElse {
|
||||
env.getHome / ".local" / "share" / LINUX_ENSO_DIRECTORY
|
||||
}
|
||||
case OS.MacOS =>
|
||||
env.getHome / "Library" / "Application Support" / MACOS_ENSO_DIRECTORY
|
||||
case OS.Windows =>
|
||||
env.getLocalAppData / WINDOWS_ENSO_DIRECTORY
|
||||
}
|
||||
}
|
||||
.toAbsolutePath
|
||||
|
||||
/**
|
||||
* Config directory for an installed distribution.
|
||||
*/
|
||||
def configDirectory: Path =
|
||||
env
|
||||
.getEnvPath(ENSO_CONFIG_DIRECTORY)
|
||||
.getOrElse {
|
||||
OS.operatingSystem match {
|
||||
case OS.Linux =>
|
||||
env
|
||||
.getEnvPath(XDG_CONFIG_DIRECTORY)
|
||||
.map(_ / LINUX_ENSO_DIRECTORY)
|
||||
.getOrElse {
|
||||
env.getHome / ".config" / LINUX_ENSO_DIRECTORY
|
||||
}
|
||||
case OS.MacOS =>
|
||||
env.getHome / "Library" / "Preferences" / MACOS_ENSO_DIRECTORY
|
||||
case OS.Windows =>
|
||||
env.getLocalAppData / WINDOWS_ENSO_DIRECTORY / CONFIG_DIRECTORY
|
||||
}
|
||||
}
|
||||
.toAbsolutePath
|
||||
|
||||
/**
|
||||
* The directory where the launcher binary will be placed for an installed
|
||||
* distribution.
|
||||
*/
|
||||
def binDirectory: Path =
|
||||
env
|
||||
.getEnvPath(ENSO_BIN_DIRECTORY)
|
||||
.getOrElse {
|
||||
OS.operatingSystem match {
|
||||
case OS.Linux =>
|
||||
env
|
||||
.getEnvPath(XDG_BIN_DIRECTORY)
|
||||
.getOrElse {
|
||||
env.getHome / ".local" / "bin"
|
||||
}
|
||||
case OS.MacOS =>
|
||||
env.getHome / ".local" / "bin"
|
||||
case OS.Windows =>
|
||||
env.getLocalAppData / WINDOWS_ENSO_DIRECTORY / BIN_DIRECTORY
|
||||
}
|
||||
}
|
||||
.toAbsolutePath
|
||||
|
||||
private def executableName: String =
|
||||
OS.executableName("enso")
|
||||
|
||||
/**
|
||||
* The path where the binary executable of the installed distribution
|
||||
* should be placed by default.
|
||||
*/
|
||||
def binaryExecutable: Path = {
|
||||
binDirectory / executableName
|
||||
}
|
||||
|
||||
/**
|
||||
* The safe version of [[dataDirectory]] which returns None if the
|
||||
* directory cannot be determined.
|
||||
*
|
||||
* Should be used in places where not being able to determine the data
|
||||
* directory is not a fatal error.
|
||||
*/
|
||||
def safeDataDirectory: Option[Path] =
|
||||
Try(dataDirectory).toOption
|
||||
|
||||
/**
|
||||
* Determines whether a locally installed distribution exists on the
|
||||
* system.
|
||||
*/
|
||||
def installedDistributionExists: Boolean =
|
||||
safeDataDirectory.exists(Files.isDirectory(_))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A default DistributionManager using the default environment.
|
||||
|
@ -1,156 +0,0 @@
|
||||
package org.enso.launcher.internal
|
||||
|
||||
import java.io.File
|
||||
import java.nio.file.Path
|
||||
|
||||
import org.enso.launcher.Logger
|
||||
|
||||
import scala.util.Try
|
||||
|
||||
/**
|
||||
* Gathers some helper methods querying the system environment.
|
||||
*
|
||||
* The default implementations should be used most of the time, but it is a
|
||||
* trait so that the functions can be overridden in tests.
|
||||
*/
|
||||
trait Environment {
|
||||
|
||||
/**
|
||||
* Returns a list of system-dependent plugin extensions.
|
||||
*
|
||||
* By default, on Unix plugins should have no extensions. On Windows, `.exe`
|
||||
* `.bat` and `.cmd` are supported.
|
||||
*/
|
||||
def getPluginExtensions: Seq[String] =
|
||||
if (OS.isWindows)
|
||||
Seq(".exe", ".bat", ".cmd")
|
||||
else Seq()
|
||||
|
||||
/**
|
||||
* Returns a list of directories that can be ignored when traversing the
|
||||
* system PATH looking for plugins.
|
||||
*
|
||||
* These could be system directories that should not contain plguins anyway,
|
||||
* but traversing them would greatly slow down plugin discovery.
|
||||
*/
|
||||
def getIgnoredPathDirectories: Seq[Path] =
|
||||
if (OS.isWindows) Seq(Path.of("C:\\Windows")) else Seq()
|
||||
|
||||
/**
|
||||
* Queries the system environment for the given variable that should
|
||||
* represent a valid filesystem path.
|
||||
*
|
||||
* If it is not defined or is not a valid path, returns None.
|
||||
*/
|
||||
def getEnvPath(key: String): Option[Path] = {
|
||||
def parsePathWithWarning(str: String): Option[Path] = {
|
||||
val result = safeParsePath(str)
|
||||
if (result.isEmpty) {
|
||||
Logger.warn(
|
||||
s"System variable `$key` was set (to value `$str`), but it did not " +
|
||||
s"represent a valid path, so it has been ignored."
|
||||
)
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
getEnvVar(key).flatMap(parsePathWithWarning)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the system PATH, if available.
|
||||
*/
|
||||
def getSystemPath: Seq[Path] =
|
||||
getEnvVar("PATH")
|
||||
.map(_.split(File.pathSeparatorChar).toSeq.flatMap(safeParsePath))
|
||||
.getOrElse(Seq())
|
||||
|
||||
/**
|
||||
* Returns the location of the HOME directory on Unix systems.
|
||||
*
|
||||
* Should not be called on Windows, as the concept of HOME should be handled
|
||||
* differently there.
|
||||
*/
|
||||
def getHome: Path = {
|
||||
if (OS.isWindows)
|
||||
throw new IllegalStateException(
|
||||
"fatal error: HOME should not be queried on Windows"
|
||||
)
|
||||
else {
|
||||
getEnvVar("HOME").flatMap(safeParsePath) match {
|
||||
case Some(path) => path
|
||||
case None =>
|
||||
throw new RuntimeException(
|
||||
"fatal error: HOME environment variable is not defined."
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the location of the local application data directory
|
||||
* (`%LocalAppData%`) on Windows.
|
||||
*
|
||||
* Should not be called on platforms other than Windows, as this concept is
|
||||
* defined in different ways there.
|
||||
*/
|
||||
def getLocalAppData: Path = {
|
||||
if (!OS.isWindows)
|
||||
throw new IllegalStateException(
|
||||
"fatal error: LocalAppData should be queried only on Windows"
|
||||
)
|
||||
else {
|
||||
getEnvVar("LocalAppData").flatMap(safeParsePath) match {
|
||||
case Some(path) => path
|
||||
case None =>
|
||||
throw new RuntimeException(
|
||||
"fatal error: %LocalAppData% environment variable is not defined."
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Queries the system environment for the given variable.
|
||||
*
|
||||
* If it is not defined or empty, returns None.
|
||||
*/
|
||||
def getEnvVar(key: String): Option[String] = {
|
||||
val value = System.getenv(key)
|
||||
if (value == null || value == "") None
|
||||
else Some(value)
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to parse a path string and returns Some(path) on success.
|
||||
*
|
||||
* We prefer silent failures here (returning None and skipping that entry),
|
||||
* as we don't want to fail the whole command if the PATH contains some
|
||||
* unparseable entries.
|
||||
*/
|
||||
private def safeParsePath(str: String): Option[Path] =
|
||||
Try(Path.of(str)).toOption
|
||||
|
||||
/**
|
||||
* Returns the path to the running program.
|
||||
*
|
||||
* It is intended for usage in native binary builds, where it returns the
|
||||
* path to the binary executable that is running. When running on the JVM,
|
||||
* returns a path to the root of the classpath for the `org.enso.launcher`
|
||||
* package or a built JAR.
|
||||
*/
|
||||
def getPathToRunningExecutable: Path = {
|
||||
try {
|
||||
val codeSource =
|
||||
this.getClass.getProtectionDomain.getCodeSource
|
||||
Path.of(codeSource.getLocation.toURI).toAbsolutePath
|
||||
} catch {
|
||||
case e: Exception =>
|
||||
throw new IllegalStateException(
|
||||
"Cannot locate the path of the launched executable",
|
||||
e
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,58 +0,0 @@
|
||||
package org.enso.launcher.internal
|
||||
|
||||
import org.enso.launcher.Logger
|
||||
|
||||
sealed trait OS
|
||||
object OS {
|
||||
case object Linux extends OS
|
||||
case object MacOS extends OS
|
||||
case object Windows extends OS
|
||||
|
||||
/**
|
||||
* The special case for when the OS is not detected.
|
||||
*
|
||||
* It is treated as a UNIX system.
|
||||
*/
|
||||
case object Unknown extends OS
|
||||
|
||||
/**
|
||||
* Checks if the application is being run on Windows.
|
||||
*/
|
||||
def isWindows: Boolean =
|
||||
operatingSystem == OS.Windows
|
||||
|
||||
/**
|
||||
* Returns which [[OS]] this program is running on.
|
||||
*/
|
||||
lazy val operatingSystem: OS = detectOS
|
||||
|
||||
private def detectOS: OS = {
|
||||
val name = System.getProperty("os.name").toLowerCase
|
||||
def nameMatches(os: OS): Boolean =
|
||||
os match {
|
||||
case Linux => name.contains("linux")
|
||||
case MacOS => name.contains("mac")
|
||||
case Windows => name.contains("windows")
|
||||
case Unknown => false
|
||||
}
|
||||
|
||||
val knownOS = Seq(Linux, MacOS, Windows)
|
||||
val possibleOS = knownOS.filter(nameMatches)
|
||||
if (possibleOS.length == 1) {
|
||||
possibleOS.head
|
||||
} else {
|
||||
Logger.warn(
|
||||
s"Could not determine a supported operating system. Assuming a UNIX " +
|
||||
s"system. Things may not work correctly."
|
||||
)
|
||||
Unknown
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps the base executable name with an optional platform-dependent
|
||||
* extension.
|
||||
*/
|
||||
def executableName(baseName: String): String =
|
||||
if (isWindows) baseName + ".exe" else baseName
|
||||
}
|
@ -0,0 +1,100 @@
|
||||
package org.enso.launcher.internal
|
||||
|
||||
import java.io.InputStream
|
||||
|
||||
/**
|
||||
* Represents a *mutable* progress status.
|
||||
*/
|
||||
trait ReadProgress {
|
||||
|
||||
/**
|
||||
* Specifies how many units have already been read.
|
||||
*
|
||||
* Querying this property over time may give different results as the task
|
||||
* progresses.
|
||||
*/
|
||||
def alreadyRead(): Long
|
||||
|
||||
/**
|
||||
* Specifies how many units in total are expected, if known.
|
||||
*/
|
||||
def total(): Option[Long]
|
||||
}
|
||||
|
||||
/**
|
||||
* A wrapper for an [[InputStream]] that tracks the read progresss.
|
||||
*
|
||||
* @param in the base stream to wrap
|
||||
* @param totalSize total amount of bytes that are expected to be available in
|
||||
* that stream
|
||||
* @param updated a callback that is called whenever progress is made
|
||||
*/
|
||||
class ProgressInputStream(
|
||||
in: InputStream,
|
||||
totalSize: Option[Long],
|
||||
updated: ReadProgress => Unit
|
||||
) extends InputStream {
|
||||
private var bytesRead: Long = 0
|
||||
|
||||
private val readProgress = new ReadProgress {
|
||||
override def alreadyRead(): Long = bytesRead
|
||||
override def total(): Option[Long] = totalSize
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the [[ReadProgress]] instance that can be queried to check how
|
||||
* many bytes have been read already.
|
||||
*/
|
||||
def progress: ReadProgress = readProgress
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
override def available: Int =
|
||||
in.available()
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
override def read: Int = {
|
||||
bytesRead += 1
|
||||
updated(readProgress)
|
||||
in.read()
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
override def read(b: Array[Byte]): Int = {
|
||||
val bytes = in.read(b)
|
||||
bytesRead += bytes
|
||||
updated(readProgress)
|
||||
bytes
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
override def read(b: Array[Byte], off: Int, len: Int): Int = {
|
||||
val bytes = in.read(b, off, len)
|
||||
bytesRead += bytes
|
||||
updated(readProgress)
|
||||
bytes
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
override def skip(n: Long): Long = {
|
||||
val skipped = in.skip(n)
|
||||
bytesRead += skipped
|
||||
updated(readProgress)
|
||||
skipped
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
override def close(): Unit =
|
||||
in.close()
|
||||
}
|
@ -1,253 +0,0 @@
|
||||
package org.enso.launcher.internal.installation
|
||||
|
||||
import java.nio.file.{Files, Path}
|
||||
|
||||
import org.enso.launcher.Logger
|
||||
import org.enso.launcher.FileSystem.PathSyntax
|
||||
import org.enso.launcher.internal.{Environment, OS}
|
||||
|
||||
import scala.util.Try
|
||||
|
||||
/**
|
||||
* Gathers filesystem paths used by the launcher.
|
||||
*
|
||||
* @param runtimes location of runtimes, corresponding to `runtime` directory
|
||||
* @param engines location of engine versions, corresponding to `dist`
|
||||
* directory
|
||||
* @param config location of configuration
|
||||
* @param dataRoot the root of the data directory; for a portable distribution
|
||||
* this is the root of the distribution, for a locally
|
||||
* installed distribution, it corresponds to `ENSO_DATA_DIR`
|
||||
*/
|
||||
case class DistributionPaths(
|
||||
dataRoot: Path,
|
||||
runtimes: Path,
|
||||
engines: Path,
|
||||
config: Path
|
||||
) {
|
||||
override def toString: String =
|
||||
s"""DistributionPaths(
|
||||
| dataRoot = $dataRoot,
|
||||
| runtimes = $runtimes,
|
||||
| engines = $engines,
|
||||
| config = $config
|
||||
|)""".stripMargin
|
||||
}
|
||||
|
||||
/**
|
||||
* A helper class that detects if a portable or installed distribution is run
|
||||
* and encapsulates management of paths to components of the distribution.
|
||||
*/
|
||||
class DistributionManager(val env: Environment) {
|
||||
|
||||
/**
|
||||
* Specifies whether the launcher has been run as a portable distribution or
|
||||
* it is a locally installed distribution.
|
||||
*/
|
||||
lazy val isRunningPortable: Boolean = {
|
||||
val portable = detectPortable()
|
||||
Logger.debug(s"Launcher portable mode = $portable")
|
||||
if (portable && LocallyInstalledDirectories.installedDistributionExists) {
|
||||
val installedRoot = LocallyInstalledDirectories.dataDirectory
|
||||
val installedBinary = LocallyInstalledDirectories.binaryExecutable
|
||||
|
||||
Logger.debug(
|
||||
s"The launcher is run in portable mode, but an installed distribution" +
|
||||
s" is available at $installedRoot."
|
||||
)
|
||||
|
||||
if (Files.exists(installedBinary)) {
|
||||
if (installedBinary == env.getPathToRunningExecutable) {
|
||||
Logger.debug(
|
||||
"That distribution seems to be corresponding to this launcher " +
|
||||
"executable, that is running in portable mode."
|
||||
)
|
||||
} else {
|
||||
Logger.debug(
|
||||
s"However, that installed distribution most likely uses another " +
|
||||
s"launcher executable, located at $installedBinary."
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
portable
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines paths that should be used by the launcher.
|
||||
*/
|
||||
lazy val paths: DistributionPaths = {
|
||||
val paths = detectPaths()
|
||||
Logger.debug(s"Detected paths are: $paths")
|
||||
paths
|
||||
}
|
||||
|
||||
private val PORTABLE_MARK_FILENAME = ".enso.portable"
|
||||
val ENGINES_DIRECTORY = "dist"
|
||||
val RUNTIMES_DIRECTORY = "runtime"
|
||||
val CONFIG_DIRECTORY = "config"
|
||||
val BIN_DIRECTORY = "bin"
|
||||
|
||||
private def detectPortable(): Boolean = Files.exists(portableMarkFilePath)
|
||||
private def possiblePortableRoot: Path =
|
||||
env.getPathToRunningExecutable.getParent.getParent
|
||||
|
||||
private def portableMarkFilePath: Path =
|
||||
possiblePortableRoot / PORTABLE_MARK_FILENAME
|
||||
|
||||
private def detectPaths(): DistributionPaths =
|
||||
if (isRunningPortable) {
|
||||
val root = env.getPathToRunningExecutable.getParent.getParent
|
||||
DistributionPaths(
|
||||
dataRoot = root,
|
||||
runtimes = root / RUNTIMES_DIRECTORY,
|
||||
engines = root / ENGINES_DIRECTORY,
|
||||
config = root / CONFIG_DIRECTORY
|
||||
)
|
||||
} else {
|
||||
val dataRoot = LocallyInstalledDirectories.dataDirectory
|
||||
val configRoot = LocallyInstalledDirectories.configDirectory
|
||||
DistributionPaths(
|
||||
dataRoot = dataRoot,
|
||||
runtimes = dataRoot / RUNTIMES_DIRECTORY,
|
||||
engines = dataRoot / ENGINES_DIRECTORY,
|
||||
config = configRoot
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* A helper for managing directories of the local installation.
|
||||
*
|
||||
* It returns paths of the local installation even if the launcher is running
|
||||
* in portable mode, so that this helper can be used by the installer to
|
||||
* determine destination for installed files.
|
||||
*/
|
||||
object LocallyInstalledDirectories {
|
||||
val ENSO_DATA_DIRECTORY = "ENSO_DATA_DIRECTORY"
|
||||
val ENSO_CONFIG_DIRECTORY = "ENSO_CONFIG_DIRECTORY"
|
||||
val ENSO_BIN_DIRECTORY = "ENSO_BIN_DIRECTORY"
|
||||
|
||||
private val XDG_DATA_DIRECTORY = "XDG_DATA_DIRECTORY"
|
||||
private val XDG_CONFIG_DIRECTORY = "XDG_CONFIG_DIRECTORY"
|
||||
private val XDG_BIN_DIRECTORY = "XDG_BIN_DIRECTORY"
|
||||
|
||||
private val LINUX_ENSO_DIRECTORY = "enso"
|
||||
private val MACOS_ENSO_DIRECTORY = "org.enso"
|
||||
private val WINDOWS_ENSO_DIRECTORY = "enso"
|
||||
|
||||
/**
|
||||
* Data directory for an installed distribution.
|
||||
*/
|
||||
def dataDirectory: Path =
|
||||
env
|
||||
.getEnvPath(ENSO_DATA_DIRECTORY)
|
||||
.getOrElse {
|
||||
OS.operatingSystem match {
|
||||
case OS.Linux =>
|
||||
env
|
||||
.getEnvPath(XDG_DATA_DIRECTORY)
|
||||
.map(_ / LINUX_ENSO_DIRECTORY)
|
||||
.getOrElse {
|
||||
env.getHome / ".local" / "share" / LINUX_ENSO_DIRECTORY
|
||||
}
|
||||
case OS.MacOS =>
|
||||
env.getHome / "Library" / "Application Support" / MACOS_ENSO_DIRECTORY
|
||||
case OS.Windows =>
|
||||
env.getLocalAppData / WINDOWS_ENSO_DIRECTORY
|
||||
case OS.Unknown =>
|
||||
throw new RuntimeException(
|
||||
"Could not determine the default installation location as " +
|
||||
s"the operating system is not recognized. Please set the" +
|
||||
s"`$ENSO_DATA_DIRECTORY` environment variable."
|
||||
)
|
||||
}
|
||||
}
|
||||
.toAbsolutePath
|
||||
|
||||
/**
|
||||
* Config directory for an installed distribution.
|
||||
*/
|
||||
def configDirectory: Path =
|
||||
env
|
||||
.getEnvPath(ENSO_CONFIG_DIRECTORY)
|
||||
.getOrElse {
|
||||
OS.operatingSystem match {
|
||||
case OS.Linux =>
|
||||
env
|
||||
.getEnvPath(XDG_CONFIG_DIRECTORY)
|
||||
.map(_ / LINUX_ENSO_DIRECTORY)
|
||||
.getOrElse {
|
||||
env.getHome / ".config" / LINUX_ENSO_DIRECTORY
|
||||
}
|
||||
case OS.MacOS =>
|
||||
env.getHome / "Library" / "Preferences" / MACOS_ENSO_DIRECTORY
|
||||
case OS.Windows =>
|
||||
env.getLocalAppData / WINDOWS_ENSO_DIRECTORY / CONFIG_DIRECTORY
|
||||
case OS.Unknown =>
|
||||
throw new RuntimeException(
|
||||
"Could not determine the default installation location as " +
|
||||
s"the operating system is not recognized. Please set the" +
|
||||
s"`$ENSO_CONFIG_DIRECTORY` environment variable."
|
||||
)
|
||||
}
|
||||
}
|
||||
.toAbsolutePath
|
||||
|
||||
/**
|
||||
* The directory where the launcher binary will be placed for an installed
|
||||
* distribution.
|
||||
*/
|
||||
def binDirectory: Path =
|
||||
env
|
||||
.getEnvPath(ENSO_BIN_DIRECTORY)
|
||||
.getOrElse {
|
||||
OS.operatingSystem match {
|
||||
case OS.Linux =>
|
||||
env
|
||||
.getEnvPath(XDG_BIN_DIRECTORY)
|
||||
.getOrElse {
|
||||
env.getHome / ".local" / "bin"
|
||||
}
|
||||
case OS.MacOS =>
|
||||
env.getHome / ".local" / "bin"
|
||||
case OS.Windows =>
|
||||
env.getLocalAppData / WINDOWS_ENSO_DIRECTORY / BIN_DIRECTORY
|
||||
case OS.Unknown =>
|
||||
throw new RuntimeException(
|
||||
"Could not determine the default installation location as " +
|
||||
s"the operating system is not recognized. Please set the" +
|
||||
s"`$ENSO_BIN_DIRECTORY` environment variable."
|
||||
)
|
||||
}
|
||||
}
|
||||
.toAbsolutePath
|
||||
|
||||
private def executableName: String =
|
||||
OS.executableName("enso")
|
||||
|
||||
/**
|
||||
* The path where the binary executable of the installed distribution
|
||||
* should be placed by default.
|
||||
*/
|
||||
def binaryExecutable: Path = {
|
||||
binDirectory / executableName
|
||||
}
|
||||
|
||||
/**
|
||||
* The safe version of [[dataDirectory]] which returns None if the
|
||||
* directory cannot be determined.
|
||||
*
|
||||
* Should be used in places where not being able to determine the data
|
||||
* directory is not a fatal error.
|
||||
*/
|
||||
def safeDataDirectory: Option[Path] =
|
||||
Try(dataDirectory).toOption
|
||||
|
||||
/**
|
||||
* Determines whether a locally installed distribution exists on the
|
||||
* system.
|
||||
*/
|
||||
def installedDistributionExists: Boolean =
|
||||
safeDataDirectory.exists(Files.isDirectory(_))
|
||||
}
|
||||
}
|
@ -0,0 +1,137 @@
|
||||
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 release a [[Release]] that allows to download assets
|
||||
*/
|
||||
case class EngineRelease(
|
||||
version: SemVer,
|
||||
manifest: Manifest,
|
||||
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.
|
||||
*/
|
||||
def findLatest(): Try[SemVer] =
|
||||
releaseProvider.listReleases().flatMap { releases =>
|
||||
val versions =
|
||||
releases.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, manifest, release)
|
||||
}
|
||||
|
||||
/**
|
||||
* Downloads the package associated with the given release into
|
||||
* `destination`.
|
||||
*
|
||||
* @param release the release to download the package from
|
||||
* @param destination name of the file that will be created to contain the
|
||||
* downloaded package
|
||||
*/
|
||||
def downloadPackage(
|
||||
release: EngineRelease,
|
||||
destination: Path
|
||||
): TaskProgress[Unit] = {
|
||||
val packageName = release.packageFileName
|
||||
release.release.assets
|
||||
.find(_.fileName == packageName)
|
||||
.map(_.downloadTo(destination))
|
||||
.getOrElse {
|
||||
TaskProgress.immediateFailure(
|
||||
ReleaseProviderException(
|
||||
s"Cannot find package `$packageName` in the release."
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Default [[EngineReleaseProvider]] that uses the GitHub Release API.
|
||||
*/
|
||||
object EngineReleaseProvider
|
||||
extends EngineReleaseProvider(
|
||||
new GithubReleaseProvider(
|
||||
"enso-org",
|
||||
"enso-staging" // TODO [RW] The release provider will be moved from
|
||||
// staging to the main repository, when the first official Enso release
|
||||
// is released.
|
||||
)
|
||||
)
|
@ -0,0 +1,72 @@
|
||||
package org.enso.launcher.releases
|
||||
|
||||
import java.nio.file.Path
|
||||
|
||||
import org.enso.cli.TaskProgress
|
||||
import org.enso.launcher.OS
|
||||
import org.enso.launcher.components.RuntimeVersion
|
||||
import org.enso.launcher.releases.github.GithubReleaseProvider
|
||||
|
||||
import scala.util.{Failure, Success}
|
||||
|
||||
/**
|
||||
* [[RuntimeReleaseProvider]] implementation providing Graal Community Edition
|
||||
* releases from the given [[ReleaseProvider]].
|
||||
*/
|
||||
class GraalCEReleaseProvider(releaseProvider: ReleaseProvider)
|
||||
extends RuntimeReleaseProvider {
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
override def packageFileName(version: RuntimeVersion): String = {
|
||||
val os = OS.operatingSystem match {
|
||||
case OS.Linux => "linux"
|
||||
case OS.MacOS => "darwin"
|
||||
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"graalvm-ce-java${version.java}-$os-$arch-${version.graal}$extension"
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
override def downloadPackage(
|
||||
version: RuntimeVersion,
|
||||
destination: Path
|
||||
): TaskProgress[Unit] = {
|
||||
val tagName = s"vm-${version.graal}"
|
||||
val packageName = packageFileName(version)
|
||||
val release = releaseProvider.releaseForTag(tagName)
|
||||
release match {
|
||||
case Failure(exception) =>
|
||||
TaskProgress.immediateFailure(exception)
|
||||
case Success(release) =>
|
||||
release.assets
|
||||
.find(_.fileName == packageName)
|
||||
.map(_.downloadTo(destination))
|
||||
.getOrElse {
|
||||
TaskProgress.immediateFailure(
|
||||
ReleaseProviderException(
|
||||
s"Cannot find package `$packageName` in the release."
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Default [[RuntimeReleaseProvider]] that provides Graal CE releases using the
|
||||
* GitHub Release API.
|
||||
*/
|
||||
object GraalCEReleaseProvider
|
||||
extends GraalCEReleaseProvider(
|
||||
new GithubReleaseProvider("graalvm", "graalvm-ce-builds")
|
||||
)
|
@ -0,0 +1,58 @@
|
||||
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]].
|
||||
*/
|
||||
trait Release {
|
||||
|
||||
/**
|
||||
* The tag identifying this release.
|
||||
*/
|
||||
def tag: String
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
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"
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
package org.enso.launcher.releases
|
||||
|
||||
import java.nio.file.Path
|
||||
|
||||
import org.enso.cli.TaskProgress
|
||||
import org.enso.launcher.components.RuntimeVersion
|
||||
|
||||
/**
|
||||
* Interface for a service providing runtime releases.
|
||||
*/
|
||||
trait RuntimeReleaseProvider {
|
||||
|
||||
/**
|
||||
* Determines filename of the package that should be downloaded from the
|
||||
* release for a given version.
|
||||
*
|
||||
* The result of this function may be system specific (the package name may
|
||||
* include the OS, for example).
|
||||
*/
|
||||
def packageFileName(version: RuntimeVersion): String
|
||||
|
||||
/**
|
||||
* Downloads a package for the given version to the provided location.
|
||||
* @param version runtime version to download
|
||||
* @param destination name of the file that will be created to contain the
|
||||
* downloaded package
|
||||
* @return [[TaskProgress]] allowing to track progress of the download and
|
||||
* wait for its completion
|
||||
*/
|
||||
def downloadPackage(
|
||||
version: RuntimeVersion,
|
||||
destination: Path
|
||||
): TaskProgress[Unit]
|
||||
}
|
@ -0,0 +1,199 @@
|
||||
package org.enso.launcher.releases.github
|
||||
|
||||
import java.nio.file.Path
|
||||
|
||||
import io.circe._
|
||||
import io.circe.parser._
|
||||
import org.apache.http.Header
|
||||
import org.enso.cli.TaskProgress
|
||||
import org.enso.launcher.http.{
|
||||
APIResponse,
|
||||
HTTPDownload,
|
||||
HTTPRequestBuilder,
|
||||
URIBuilder
|
||||
}
|
||||
import org.enso.launcher.releases.ReleaseProviderException
|
||||
|
||||
import scala.util.{Success, Try}
|
||||
|
||||
/**
|
||||
* Contains functions used to query the GitHubAPI endpoints.
|
||||
*/
|
||||
object GithubAPI {
|
||||
|
||||
/**
|
||||
* Represents a GitHub repository.
|
||||
*
|
||||
* @param owner owner of the repository
|
||||
* @param name name of the repository
|
||||
*/
|
||||
case class Repository(owner: String, name: String)
|
||||
|
||||
/**
|
||||
* Represents a GitHub release.
|
||||
*
|
||||
* @param tag tag associated with the release
|
||||
* @param assets sequence of assets present in this release
|
||||
*/
|
||||
case class Release(tag: String, assets: Seq[Asset])
|
||||
|
||||
/**
|
||||
* Represents an asset available in a [[Release]]
|
||||
*
|
||||
* @param name filename of that asset
|
||||
* @param url URL that can be used to download this asset
|
||||
* @param size size of the asset in bytes
|
||||
*/
|
||||
case class Asset(name: String, url: String, size: Long)
|
||||
|
||||
/**
|
||||
* Returns a list of all releases in the repository.
|
||||
*
|
||||
* It fetches all available pages of releases, to make sure all releases are
|
||||
* included. This is necessary, because the GitHub API does not guarantee
|
||||
* that the releases are returned in order. The 'empirical' order seems to be
|
||||
* 'latest first', but even assuming that is a too weak guarantee, because
|
||||
* *theoretically* so many patches for an earlier release could be released,
|
||||
* that the latest release in the semver sense will not make it to the first
|
||||
* page. So to be absolutely sure that we can find the latest release, we
|
||||
* need to list all of them.
|
||||
*
|
||||
* The endpoint is blocking, because we cannot estimate how many requests
|
||||
* will be necessary and each individual request should be quick as it just
|
||||
* downloads some text, so there is not much gain in displaying a progress
|
||||
* bar for this task.
|
||||
*/
|
||||
def listReleases(repository: Repository): Try[Seq[Release]] = {
|
||||
val perPage = 100
|
||||
def listPage(page: Int): Try[Seq[Release]] = {
|
||||
val uri = (projectURI(repository) / "releases") ?
|
||||
("per_page" -> perPage.toString) ? ("page" -> page.toString)
|
||||
|
||||
HTTPDownload
|
||||
.fetchString(HTTPRequestBuilder.fromURI(uri.build()).GET)
|
||||
.flatMap(response =>
|
||||
parse(response.content)
|
||||
.flatMap(
|
||||
_.as[Seq[Release]].left
|
||||
.map(err =>
|
||||
handleError(
|
||||
response,
|
||||
ReleaseProviderException(s"Cannot fetch release list.", err)
|
||||
)
|
||||
)
|
||||
)
|
||||
.toTry
|
||||
)
|
||||
.waitForResult()
|
||||
}
|
||||
|
||||
def listAllPages(from: Int): Try[Seq[Release]] =
|
||||
listPage(from).flatMap { current =>
|
||||
if (current.length == perPage)
|
||||
listAllPages(from + 1).map(current ++ _)
|
||||
else
|
||||
Success(current)
|
||||
}
|
||||
|
||||
listAllPages(1)
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches release metadata for the release associated with the given tag.
|
||||
*/
|
||||
def getRelease(repo: Repository, tag: String): TaskProgress[Release] = {
|
||||
val uri = projectURI(repo) / "releases" / "tags" / tag
|
||||
|
||||
HTTPDownload
|
||||
.fetchString(HTTPRequestBuilder.fromURI(uri.build()).GET)
|
||||
.flatMap(response =>
|
||||
parse(response.content)
|
||||
.flatMap(_.as[Release])
|
||||
.left
|
||||
.map(err =>
|
||||
handleError(
|
||||
response,
|
||||
ReleaseProviderException(s"Cannot find release `$tag`.", err)
|
||||
)
|
||||
)
|
||||
.toTry
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* A helper function that detecte a rate-limit error and tries to make a more
|
||||
* friendly user message.
|
||||
*
|
||||
* If the rate-limit is hit, an error message is returned by the API which is
|
||||
* of course not parsed correctly by a reader that expects a normal response.
|
||||
* The rate-limit is detected and if it caused the error, the error is
|
||||
* overridden with a message explaining the rate-limit.
|
||||
*/
|
||||
private def handleError(
|
||||
response: APIResponse,
|
||||
defaultError: => Throwable
|
||||
): Throwable = {
|
||||
def isLimitExceeded(header: Header): Boolean =
|
||||
header.getValue.toIntOption.contains(0)
|
||||
|
||||
response.headers.find(_.getName == "X-RateLimit-Remaining") match {
|
||||
case Some(header) if isLimitExceeded(header) =>
|
||||
ReleaseProviderException(
|
||||
"GitHub Release API rate limit exceeded for your IP address. " +
|
||||
"Please try again in a while."
|
||||
)
|
||||
case _ => defaultError
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches an asset as text.
|
||||
*
|
||||
* Returns a [[TaskProgress]] that will return the asset contents as a
|
||||
* [[String]] on success.
|
||||
*/
|
||||
def fetchTextAsset(asset: Asset): TaskProgress[String] = {
|
||||
val request = HTTPRequestBuilder
|
||||
.fromURIString(asset.url)
|
||||
.addHeader("Accept", "application/octet-stream")
|
||||
.GET
|
||||
|
||||
HTTPDownload.fetchString(request, Some(asset.size)).map(_.content)
|
||||
}
|
||||
|
||||
/**
|
||||
* Downloads the asset to the provided `destination`.
|
||||
*
|
||||
* The returned [[TaskProgress]] succeeds iff the download was successful.
|
||||
*/
|
||||
def downloadAsset(asset: Asset, destination: Path): TaskProgress[Unit] = {
|
||||
val request = HTTPRequestBuilder
|
||||
.fromURIString(asset.url)
|
||||
.addHeader("Accept", "application/octet-stream")
|
||||
.GET
|
||||
|
||||
HTTPDownload
|
||||
.download(request, destination, Some(asset.size))
|
||||
.map(_ => ())
|
||||
}
|
||||
|
||||
private val baseUrl =
|
||||
URIBuilder.fromHost("api.github.com")
|
||||
private def projectURI(project: Repository) =
|
||||
baseUrl / "repos" / project.owner / project.name
|
||||
|
||||
implicit private val assetDecoder: Decoder[Asset] = { json =>
|
||||
for {
|
||||
url <- json.get[String]("browser_download_url")
|
||||
name <- json.get[String]("name")
|
||||
size <- json.get[Long]("size")
|
||||
} yield Asset(name = name, url = url, size = size)
|
||||
}
|
||||
|
||||
implicit private val releaseDecoder: Decoder[Release] = { json =>
|
||||
for {
|
||||
tag <- json.get[String]("tag_name")
|
||||
assets <- json.get[Seq[Asset]]("assets")
|
||||
} yield Release(tag, assets)
|
||||
}
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
package org.enso.launcher.releases.github
|
||||
|
||||
import java.nio.file.Path
|
||||
|
||||
import org.enso.cli.TaskProgress
|
||||
import org.enso.launcher.releases.Asset
|
||||
|
||||
case class GithubAsset(asset: GithubAPI.Asset) extends Asset {
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
override def fileName: String = asset.name
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
override def downloadTo(path: Path): TaskProgress[Unit] =
|
||||
GithubAPI.downloadAsset(asset, path)
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
override def fetchAsText(): TaskProgress[String] =
|
||||
GithubAPI.fetchTextAsset(asset)
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
package org.enso.launcher.releases.github
|
||||
|
||||
import org.enso.launcher.releases.{Asset, Release}
|
||||
|
||||
case class GithubRelease(release: GithubAPI.Release) extends Release {
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
override def tag: String = release.tag
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
override def assets: Seq[Asset] = release.assets.map(GithubAsset)
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
package org.enso.launcher.releases.github
|
||||
|
||||
import org.enso.launcher.releases.{Release, ReleaseProvider}
|
||||
|
||||
import scala.util.Try
|
||||
|
||||
/**
|
||||
* Implements [[ReleaseProvider]] providing releases from a specified GitHub
|
||||
* repository using the GitHub Release API.
|
||||
*
|
||||
* @param owner owner of the repository
|
||||
* @param repositoryName name of the repository
|
||||
*/
|
||||
class GithubReleaseProvider(
|
||||
owner: String,
|
||||
repositoryName: String
|
||||
) extends ReleaseProvider {
|
||||
private val repo = GithubAPI.Repository(owner, repositoryName)
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
override def releaseForTag(tag: String): Try[Release] =
|
||||
GithubAPI.getRelease(repo, tag).waitForResult().map(GithubRelease)
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
override def listReleases(): Try[Seq[Release]] =
|
||||
GithubAPI.listReleases(repo).map(_.map(GithubRelease))
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
minimum-launcher-version: 0.0.1
|
||||
graal-vm-version: 1.0.0
|
||||
graal-java-version: 11
|
@ -0,0 +1,3 @@
|
||||
minimum-launcher-version: 0.0.1
|
||||
graal-vm-version: 1.0.0
|
||||
graal-java-version: 11
|
@ -0,0 +1,3 @@
|
||||
minimum-launcher-version: 0.0.1
|
||||
graal-vm-version: 1.0.0
|
||||
graal-java-version: 11
|
@ -0,0 +1,3 @@
|
||||
minimum-launcher-version: 0.0.1
|
||||
graal-vm-version: 1.0.0
|
||||
graal-java-version: 11
|
@ -0,0 +1,3 @@
|
||||
minimum-launcher-version: 0.0.1
|
||||
graal-vm-version: 2.0.0
|
||||
graal-java-version: 11
|
@ -0,0 +1,3 @@
|
||||
minimum-launcher-version: 0.0.1
|
||||
graal-vm-version: 2.0.0
|
||||
graal-java-version: 11
|
@ -0,0 +1,3 @@
|
||||
minimum-launcher-version: 0.0.1
|
||||
graal-vm-version: 2.0.0
|
||||
graal-java-version: 11
|
@ -0,0 +1,3 @@
|
||||
minimum-launcher-version: 0.0.1
|
||||
graal-vm-version: 2.0.0
|
||||
graal-java-version: 11
|
@ -0,0 +1,3 @@
|
||||
minimum-launcher-version: 0.0.1
|
||||
graal-vm-version: 2.0.0
|
||||
graal-java-version: 11
|
@ -0,0 +1,3 @@
|
||||
minimum-launcher-version: 0.0.1
|
||||
graal-vm-version: 2.0.0
|
||||
graal-java-version: 11
|
@ -0,0 +1,3 @@
|
||||
minimum-launcher-version: 0.0.1
|
||||
graal-vm-version: 2.0.0
|
||||
graal-java-version: 11
|
@ -0,0 +1,3 @@
|
||||
minimum-launcher-version: 0.0.1
|
||||
graal-vm-version: 2.0.0
|
||||
graal-java-version: 11
|
@ -0,0 +1,2 @@
|
||||
#!/bin/sh
|
||||
echo "Hello"
|
@ -0,0 +1,2 @@
|
||||
#!/bin/sh
|
||||
echo "Hello"
|
@ -0,0 +1 @@
|
||||
placeholder
|
@ -0,0 +1,2 @@
|
||||
#!/bin/sh
|
||||
echo "Hello"
|
@ -0,0 +1,2 @@
|
||||
#!/bin/sh
|
||||
echo "Hello"
|
@ -0,0 +1 @@
|
||||
placeholder
|
@ -0,0 +1,53 @@
|
||||
package org.enso.launcher
|
||||
|
||||
import java.nio.file.{Files, Path}
|
||||
import org.enso.launcher.FileSystem.PathSyntax
|
||||
|
||||
/**
|
||||
* A test-suite mixin that adds helper functions that create a fake environment
|
||||
* which points to an Enso installation inside the temporary directory
|
||||
* generated for the test.
|
||||
*/
|
||||
trait FakeEnvironment { self: WithTemporaryDirectory =>
|
||||
|
||||
/**
|
||||
* Returns a fake path to the Enso executable that is inside the temporary
|
||||
* directory for the test.
|
||||
*
|
||||
* @param portable specifies whether the distribution should be marked as
|
||||
* portable
|
||||
*/
|
||||
def fakeExecutablePath(portable: Boolean = false): Path = {
|
||||
val fakeBin = getTestDirectory / "bin"
|
||||
Files.createDirectories(fakeBin)
|
||||
if (portable) {
|
||||
FileSystem.writeTextFile(getTestDirectory / ".enso.portable", "mark")
|
||||
}
|
||||
fakeBin / "enso"
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an [[Environment]] instance that overrides the `ENSO_*`
|
||||
* directories to be inside the temporary directory for the test.
|
||||
*/
|
||||
def fakeInstalledEnvironment(): Environment = {
|
||||
val executable = fakeExecutablePath()
|
||||
val dataDir = getTestDirectory / "test_data"
|
||||
val configDir = getTestDirectory / "test_config"
|
||||
val binDir = getTestDirectory / "test_bin"
|
||||
val fakeEnvironment = new Environment {
|
||||
override def getPathToRunningExecutable: Path = executable
|
||||
|
||||
override def getEnvVar(key: String): Option[String] =
|
||||
key match {
|
||||
case "ENSO_DATA_DIRECTORY" => Some(dataDir.toString)
|
||||
case "ENSO_CONFIG_DIRECTORY" => Some(configDir.toString)
|
||||
case "ENSO_BIN_DIRECTORY" => Some(binDir.toString)
|
||||
case _ => super.getEnvVar(key)
|
||||
}
|
||||
}
|
||||
|
||||
fakeEnvironment
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
package org.enso.launcher
|
||||
|
||||
import java.nio.file.attribute.PosixFilePermissions
|
||||
|
||||
import org.scalatest.matchers.should.Matchers
|
||||
import org.scalatest.wordspec.AnyWordSpec
|
||||
import scala.jdk.CollectionConverters._
|
||||
|
||||
class FileSystemSpec extends AnyWordSpec with Matchers {
|
||||
"decodePOSIXPermissions" should {
|
||||
def assertPair(octal: String, textual: String): Unit = {
|
||||
val mode = Integer.parseInt(octal, 8)
|
||||
val decoded = FileSystem.decodePOSIXPermissions(mode)
|
||||
val reference = PosixFilePermissions.fromString(textual)
|
||||
decoded.asScala.toSet shouldEqual reference.asScala.toSet
|
||||
}
|
||||
|
||||
"decode permissions correctly" in {
|
||||
assertPair("000", "---------")
|
||||
assertPair("111", "--x--x--x")
|
||||
assertPair("222", "-w--w--w-")
|
||||
assertPair("444", "r--r--r--")
|
||||
assertPair("777", "rwxrwxrwx")
|
||||
|
||||
assertPair("644", "rw-r--r--")
|
||||
assertPair("012", "-----x-w-")
|
||||
assertPair("421", "r---w---x")
|
||||
}
|
||||
}
|
||||
}
|
@ -10,10 +10,12 @@ class LauncherSpec
|
||||
|
||||
"new command" should {
|
||||
"create a new project with correct structure" in {
|
||||
val projectDir = getTestDirectory.resolve("proj1")
|
||||
Launcher.newProject("TEST", Some(projectDir))
|
||||
projectDir.toFile should exist
|
||||
projectDir.resolve("src").resolve("Main.enso").toFile should exist
|
||||
Logger.suppressWarnings {
|
||||
val projectDir = getTestDirectory.resolve("proj1")
|
||||
Launcher.newProject("TEST", Some(projectDir))
|
||||
projectDir.toFile should exist
|
||||
projectDir.resolve("src").resolve("Main.enso").toFile should exist
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
package org.enso.launcher
|
||||
|
||||
import java.nio.file.{Files, Path}
|
||||
import java.lang.{ProcessBuilder => JProcessBuilder}
|
||||
|
||||
import org.scalatest.concurrent.{Signaler, TimeLimitedTests}
|
||||
import org.scalatest.matchers.should.Matchers
|
||||
@ -8,10 +9,11 @@ import org.scalatest.matchers.{MatchResult, Matcher}
|
||||
import org.scalatest.time.Span
|
||||
import org.scalatest.wordspec.AnyWordSpec
|
||||
|
||||
import scala.sys.process._
|
||||
import org.scalatest.time.SpanSugar._
|
||||
|
||||
import org.enso.launcher.internal.OS
|
||||
import scala.collection.Factory
|
||||
import scala.jdk.CollectionConverters._
|
||||
import scala.jdk.StreamConverters._
|
||||
|
||||
/**
|
||||
* Contains helper methods for creating tests that need to run the native
|
||||
@ -109,6 +111,12 @@ trait NativeTest extends AnyWordSpec with Matchers with TimeLimitedTests {
|
||||
|
||||
/**
|
||||
* Creates a copy of the tested launcher binary at the specified location.
|
||||
*
|
||||
* It waits a 100ms delay after creating the 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.
|
||||
*/
|
||||
def copyLauncherTo(path: Path): Unit = {
|
||||
val parent = path.getParent
|
||||
@ -117,6 +125,7 @@ trait NativeTest extends AnyWordSpec with Matchers with TimeLimitedTests {
|
||||
if (!Files.isExecutable(path)) {
|
||||
throw new RuntimeException("Failed to make it executable...")
|
||||
}
|
||||
Thread.sleep(100)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -139,43 +148,63 @@ trait NativeTest extends AnyWordSpec with Matchers with TimeLimitedTests {
|
||||
}
|
||||
|
||||
/**
|
||||
* This property can be temporarily set to true to allow for easier debugging
|
||||
* of native tests.
|
||||
* Runs the provided `command`.
|
||||
*
|
||||
* `extraEnv` may be provided to extend the environment. Care must be taken
|
||||
* on Windows where environment variables are (mostly) case-insensitive.
|
||||
*
|
||||
* If `waitForDescendants` is set, tries to wait for descendants of the
|
||||
* launched process to finish too. Especially important on Windows where
|
||||
* child processes may run after the launcher parent has been terminated.
|
||||
*/
|
||||
private val launcherDebugLogging = false
|
||||
|
||||
private def run(
|
||||
command: Seq[String],
|
||||
extraEnv: Seq[(String, String)]
|
||||
extraEnv: Seq[(String, String)],
|
||||
waitForDescendants: Boolean = true
|
||||
): RunResult = {
|
||||
val stdout = new StringBuilder
|
||||
val stderr = new StringBuilder
|
||||
val logger = new ProcessLogger {
|
||||
override def out(s: => String): Unit = {
|
||||
if (launcherDebugLogging) {
|
||||
System.err.println(s)
|
||||
}
|
||||
stdout.append(s + "\n")
|
||||
}
|
||||
val builder = new JProcessBuilder(command: _*)
|
||||
val newKeys = extraEnv.map(_._1.toLowerCase)
|
||||
if (newKeys.distinct.size < newKeys.size) {
|
||||
throw new IllegalArgumentException(
|
||||
"The extra environment keys have to be unique"
|
||||
)
|
||||
}
|
||||
|
||||
override def err(s: => String): Unit = {
|
||||
if (launcherDebugLogging) {
|
||||
System.err.println(s)
|
||||
}
|
||||
stderr.append(s + "\n")
|
||||
}
|
||||
lazy val existingKeys =
|
||||
builder.environment().keySet().asScala
|
||||
for ((key, value) <- extraEnv) {
|
||||
if (OS.isWindows) {
|
||||
def shadows(key1: String, key2: String): Boolean =
|
||||
key1.toLowerCase == key2.toLowerCase && key1 != key2
|
||||
|
||||
override def buffer[T](f: => T): T = f
|
||||
existingKeys.find(shadows(_, key)) match {
|
||||
case Some(oldKey) =>
|
||||
throw new IllegalArgumentException(
|
||||
s"The environment key `$key` may be shadowed by `$oldKey` " +
|
||||
s"already existing in the environment. Please use `$oldKey`."
|
||||
)
|
||||
case None =>
|
||||
}
|
||||
}
|
||||
builder.environment().put(key, value)
|
||||
}
|
||||
|
||||
try {
|
||||
val process = Process(command, None, extraEnv: _*).run(logger)
|
||||
val process = builder.start()
|
||||
|
||||
try {
|
||||
RunResult(process.exitValue(), stdout.toString(), stderr.toString())
|
||||
val exitCode = process.waitFor()
|
||||
if (waitForDescendants) {
|
||||
val descendants = process.descendants().toScala(Factory.arrayFactory)
|
||||
descendants.foreach(_.onExit().join())
|
||||
}
|
||||
val stdout = new String(process.getInputStream.readAllBytes())
|
||||
val stderr = new String(process.getErrorStream.readAllBytes())
|
||||
RunResult(exitCode, stdout, stderr)
|
||||
} catch {
|
||||
case e: InterruptedException =>
|
||||
if (process.isAlive()) {
|
||||
println("Killing the timed-out process")
|
||||
if (process.isAlive) {
|
||||
println(s"Killing the timed-out process: ${command.mkString(" ")}")
|
||||
process.destroy()
|
||||
}
|
||||
throw e
|
||||
|
@ -6,21 +6,41 @@ import java.io.{File, IOException}
|
||||
import org.apache.commons.io.FileUtils
|
||||
import org.scalatest.{BeforeAndAfterEach, Suite}
|
||||
|
||||
/**
|
||||
* Creates a separate temporary directory for each test.
|
||||
*/
|
||||
trait WithTemporaryDirectory extends Suite with BeforeAndAfterEach {
|
||||
private var testDirectory: Path = _
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
override def beforeEach(): Unit = {
|
||||
super.beforeEach()
|
||||
testDirectory = Files.createTempDirectory("tmptest")
|
||||
testDirectory = Files.createTempDirectory("enso-test")
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
override def afterEach(): Unit = {
|
||||
super.afterEach()
|
||||
robustDeleteDirectory(testDirectory.toFile)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the temporary directory for this test.
|
||||
*/
|
||||
def getTestDirectory: Path = testDirectory.toAbsolutePath
|
||||
|
||||
/**
|
||||
* Tries to remove the directory, retrying every 100ms for 3 seconds.
|
||||
*
|
||||
* This is used because there may be some lag between the test finalizing and
|
||||
* the filesystem allowing to remove the files (especially on Windows,
|
||||
* because if the test runs other executables, they may take a moment to
|
||||
* terminate even after the test completed).
|
||||
*/
|
||||
private def robustDeleteDirectory(dir: File): Unit = {
|
||||
def tryRemoving(retry: Int): Unit = {
|
||||
try {
|
||||
|
@ -0,0 +1,153 @@
|
||||
package org.enso.launcher.components
|
||||
|
||||
import java.nio.file.Path
|
||||
|
||||
import nl.gn0s1s.bump.SemVer
|
||||
import org.enso.launcher.cli.GlobalCLIOptions
|
||||
import org.enso.launcher.installation.DistributionManager
|
||||
import org.enso.launcher.releases.{
|
||||
EngineReleaseProvider,
|
||||
GraalCEReleaseProvider
|
||||
}
|
||||
import org.enso.launcher.{FakeEnvironment, Logger, WithTemporaryDirectory}
|
||||
import org.scalatest.OptionValues
|
||||
import org.scalatest.matchers.should.Matchers
|
||||
import org.scalatest.wordspec.AnyWordSpec
|
||||
|
||||
class ComponentsManagerSpec
|
||||
extends AnyWordSpec
|
||||
with Matchers
|
||||
with OptionValues
|
||||
with WithTemporaryDirectory
|
||||
with FakeEnvironment {
|
||||
|
||||
private def makeManagers(): (DistributionManager, ComponentsManager) = {
|
||||
val distributionManager = new DistributionManager(
|
||||
fakeInstalledEnvironment()
|
||||
)
|
||||
val fakeReleasesRoot =
|
||||
Path.of(
|
||||
getClass
|
||||
.getResource("fake-releases")
|
||||
.toURI
|
||||
)
|
||||
val engineProvider = new EngineReleaseProvider(
|
||||
FakeReleaseProvider(fakeReleasesRoot.resolve("enso"))
|
||||
)
|
||||
val runtimeProvider = new GraalCEReleaseProvider(
|
||||
FakeReleaseProvider(fakeReleasesRoot.resolve("graalvm"))
|
||||
)
|
||||
val componentsManager = new ComponentsManager(
|
||||
GlobalCLIOptions(autoConfirm = true, hideProgress = true),
|
||||
distributionManager,
|
||||
engineProvider,
|
||||
runtimeProvider
|
||||
)
|
||||
|
||||
(distributionManager, componentsManager)
|
||||
}
|
||||
|
||||
def makeComponentsManager(): ComponentsManager = makeManagers()._2
|
||||
|
||||
"ComponentsManager" should {
|
||||
"find the latest engine version in semver ordering" in {
|
||||
Logger.suppressWarnings {
|
||||
val componentsManager = makeComponentsManager()
|
||||
componentsManager.fetchLatestEngineVersion() shouldEqual SemVer(0, 0, 1)
|
||||
}
|
||||
}
|
||||
|
||||
"install the engine and a matching runtime for it" in {
|
||||
Logger.suppressWarnings {
|
||||
val (distributionManager, componentsManager) = makeManagers()
|
||||
|
||||
val version = SemVer(0, 0, 1)
|
||||
val engine = componentsManager.findOrInstallEngine(SemVer(0, 0, 1))
|
||||
|
||||
engine.version shouldEqual version
|
||||
assert(
|
||||
engine.path.startsWith(distributionManager.paths.engines),
|
||||
"Engine should be installed in the engines directory."
|
||||
)
|
||||
|
||||
val runtime = componentsManager.findRuntime(engine)
|
||||
runtime.value.version shouldEqual RuntimeVersion(SemVer(2, 0, 0), "11")
|
||||
assert(
|
||||
runtime.value.path.startsWith(distributionManager.paths.runtimes),
|
||||
"Engine should be installed in the engines directory."
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
"list installed engines and runtimes" in {
|
||||
Logger.suppressWarnings {
|
||||
val componentsManager = makeComponentsManager()
|
||||
val engineVersions =
|
||||
Set(SemVer(0, 0, 0), SemVer(0, 0, 1), SemVer(0, 0, 1, Some("pre")))
|
||||
val runtimeVersions =
|
||||
Set(
|
||||
RuntimeVersion(SemVer(1, 0, 0), "11"),
|
||||
RuntimeVersion(SemVer(2, 0, 0), "11")
|
||||
)
|
||||
engineVersions.map(
|
||||
componentsManager.findOrInstallEngine(_, complain = false)
|
||||
)
|
||||
|
||||
componentsManager
|
||||
.listInstalledEngines()
|
||||
.map(_.version)
|
||||
.toSet shouldEqual engineVersions
|
||||
componentsManager
|
||||
.listInstalledRuntimes()
|
||||
.map(_.version)
|
||||
.toSet shouldEqual runtimeVersions
|
||||
|
||||
val runtime2 =
|
||||
componentsManager
|
||||
.findRuntime(RuntimeVersion(SemVer(2, 0, 0), "11"))
|
||||
.value
|
||||
componentsManager.findEnginesUsingRuntime(runtime2) should have length 2
|
||||
}
|
||||
}
|
||||
|
||||
"uninstall the runtime iff it is not used by any engines" in {
|
||||
Logger.suppressWarnings {
|
||||
val componentsManager = makeComponentsManager()
|
||||
val engineVersions =
|
||||
Seq(SemVer(0, 0, 0), SemVer(0, 0, 1), SemVer(0, 0, 1, Some("pre")))
|
||||
engineVersions.map(
|
||||
componentsManager.findOrInstallEngine(_, complain = false)
|
||||
)
|
||||
|
||||
componentsManager.listInstalledEngines() should have length 3
|
||||
componentsManager.listInstalledRuntimes() should have length 2
|
||||
|
||||
// remove the engine that shares the runtime with another one
|
||||
val version1 = SemVer(0, 0, 1, Some("pre"))
|
||||
componentsManager.uninstallEngine(version1)
|
||||
val engines1 = componentsManager.listInstalledEngines()
|
||||
engines1 should have length 2
|
||||
engines1.map(_.version) should not contain version1
|
||||
componentsManager.listInstalledRuntimes() should have length 2
|
||||
|
||||
// remove the second engine that shared the runtime
|
||||
val version2 = SemVer(0, 0, 1)
|
||||
componentsManager.uninstallEngine(version2)
|
||||
val engines2 = componentsManager.listInstalledEngines()
|
||||
engines2 should have length 1
|
||||
engines2.map(_.version) should not contain version2
|
||||
val runtimes2 = componentsManager.listInstalledRuntimes()
|
||||
runtimes2 should have length 1
|
||||
runtimes2.map(_.version).head shouldEqual RuntimeVersion(
|
||||
SemVer(1, 0, 0),
|
||||
"11"
|
||||
)
|
||||
|
||||
// remove the last engine
|
||||
componentsManager.uninstallEngine(SemVer(0, 0, 0))
|
||||
componentsManager.listInstalledEngines() should have length 0
|
||||
componentsManager.listInstalledRuntimes() should have length 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,136 @@
|
||||
package org.enso.launcher.components
|
||||
|
||||
import java.nio.file.{Files, Path}
|
||||
|
||||
import org.enso.cli.{ProgressListener, TaskProgress}
|
||||
import org.enso.launcher.{FileSystem, OS}
|
||||
import org.enso.launcher.releases.{
|
||||
Asset,
|
||||
Release,
|
||||
ReleaseProvider,
|
||||
ReleaseProviderException
|
||||
}
|
||||
|
||||
import scala.io.Source
|
||||
import scala.util.{Success, Try, Using}
|
||||
import sys.process._
|
||||
|
||||
case class FakeReleaseProvider(releasesRoot: Path) extends ReleaseProvider {
|
||||
private val releases = FileSystem.listDirectory(releasesRoot).map(FakeRelease)
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
override def releaseForTag(tag: String): Try[Release] =
|
||||
releases
|
||||
.find(_.tag == tag)
|
||||
.toRight(ReleaseProviderException(s"Release $tag does not exist."))
|
||||
.toTry
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
override def listReleases(): Try[Seq[Release]] = Success(releases)
|
||||
}
|
||||
|
||||
case class FakeRelease(path: Path) extends Release {
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
override def tag: String = path.getFileName.toString
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
override def assets: Seq[Asset] =
|
||||
FileSystem.listDirectory(path).map(FakeAsset)
|
||||
}
|
||||
|
||||
case class FakeAsset(source: Path) extends Asset {
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
override def fileName: String = source.getFileName.toString
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
override def downloadTo(path: Path): TaskProgress[Unit] = {
|
||||
val result = Try(copyFakeAsset(path))
|
||||
new TaskProgress[Unit] {
|
||||
override def addProgressListener(
|
||||
listener: ProgressListener[Unit]
|
||||
): Unit = {
|
||||
listener.done(result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private def copyFakeAsset(destination: Path): Unit =
|
||||
if (Files.isDirectory(source)) {
|
||||
val directoryName = source.getFileName.toString
|
||||
if (directoryName.endsWith(".tar.gz") && OS.isUNIX)
|
||||
packTarGz(source, destination)
|
||||
else if (directoryName.endsWith(".zip") && OS.isWindows)
|
||||
packZip(source, destination)
|
||||
else {
|
||||
throw new IllegalArgumentException(
|
||||
s"Fake-archive format $directoryName is not supported on " +
|
||||
s"${OS.operatingSystem}."
|
||||
)
|
||||
}
|
||||
} else {
|
||||
FileSystem.copyFile(source, destination)
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
override def fetchAsText(): TaskProgress[String] = {
|
||||
val txt = Using(Source.fromFile(source.toFile)) { src =>
|
||||
src.getLines().mkString("\n")
|
||||
}
|
||||
(listener: ProgressListener[String]) => {
|
||||
listener.done(txt)
|
||||
}
|
||||
}
|
||||
|
||||
private def packTarGz(source: Path, destination: Path): Unit = {
|
||||
val files = FileSystem.listDirectory(source)
|
||||
val exitCode = Process(
|
||||
Seq(
|
||||
"tar",
|
||||
"-czf",
|
||||
destination.toAbsolutePath.toString
|
||||
) ++ files.map(_.getFileName.toString),
|
||||
source.toFile
|
||||
).!
|
||||
if (exitCode != 0) {
|
||||
throw new RuntimeException(
|
||||
s"tar failed. Cannot create fake-archive for $source"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private def packZip(source: Path, destination: Path): Unit = {
|
||||
val files = FileSystem.listDirectory(source)
|
||||
val exitCode = Process(
|
||||
Seq(
|
||||
"powershell",
|
||||
"Compress-Archive",
|
||||
"-Path",
|
||||
files.map(_.getFileName.toString).mkString(","),
|
||||
"-DestinationPath",
|
||||
destination.toAbsolutePath.toString
|
||||
),
|
||||
source.toFile
|
||||
).!
|
||||
if (exitCode != 0) {
|
||||
throw new RuntimeException(
|
||||
s"tar failed. Cannot create fake-archive for $source"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,32 +1,24 @@
|
||||
package org.enso.launcher.installation
|
||||
|
||||
import java.nio.file.{Files, Path}
|
||||
import java.nio.file.Path
|
||||
|
||||
import org.enso.launcher.{FileSystem, WithTemporaryDirectory}
|
||||
import org.enso.launcher.FileSystem.PathSyntax
|
||||
import org.enso.launcher.{Environment, FakeEnvironment, WithTemporaryDirectory}
|
||||
import org.scalatest.matchers.should.Matchers
|
||||
import org.scalatest.wordspec.AnyWordSpec
|
||||
import org.enso.launcher.FileSystem.PathSyntax
|
||||
import org.enso.launcher.internal.Environment
|
||||
import org.enso.launcher.internal.installation.DistributionManager
|
||||
|
||||
class DistributionManagerSpec
|
||||
extends AnyWordSpec
|
||||
with Matchers
|
||||
with WithTemporaryDirectory {
|
||||
|
||||
def fakeExecutablePath(): Path = {
|
||||
val fakeBin = getTestDirectory / "bin"
|
||||
Files.createDirectories(fakeBin)
|
||||
fakeBin / "enso"
|
||||
}
|
||||
with WithTemporaryDirectory
|
||||
with FakeEnvironment {
|
||||
|
||||
"DistributionManager" should {
|
||||
"detect portable distribution" in {
|
||||
val executable = fakeExecutablePath()
|
||||
val executable = fakeExecutablePath(portable = true)
|
||||
val fakeEnvironment = new Environment {
|
||||
override def getPathToRunningExecutable: Path = executable
|
||||
}
|
||||
FileSystem.writeTextFile(getTestDirectory / ".enso.portable", "mark")
|
||||
|
||||
val distributionManager = new DistributionManager(fakeEnvironment)
|
||||
distributionManager.isRunningPortable shouldEqual true
|
||||
@ -49,23 +41,12 @@ class DistributionManagerSpec
|
||||
|
||||
"respect environment variable overrides " +
|
||||
"for installed distribution location" in {
|
||||
val executable = fakeExecutablePath()
|
||||
val dataDir = getTestDirectory / "test_data"
|
||||
val configDir = getTestDirectory / "test_config"
|
||||
val binDir = getTestDirectory / "test_bin"
|
||||
val fakeEnvironment = new Environment {
|
||||
override def getPathToRunningExecutable: Path = executable
|
||||
val dataDir = getTestDirectory / "test_data"
|
||||
val configDir = getTestDirectory / "test_config"
|
||||
val binDir = getTestDirectory / "test_bin"
|
||||
|
||||
override def getEnvVar(key: String): Option[String] =
|
||||
key match {
|
||||
case "ENSO_DATA_DIRECTORY" => Some(dataDir.toString)
|
||||
case "ENSO_CONFIG_DIRECTORY" => Some(configDir.toString)
|
||||
case "ENSO_BIN_DIRECTORY" => Some(binDir.toString)
|
||||
case _ => super.getEnvVar(key)
|
||||
}
|
||||
}
|
||||
|
||||
val distributionManager = new DistributionManager(fakeEnvironment)
|
||||
val distributionManager =
|
||||
new DistributionManager(fakeInstalledEnvironment())
|
||||
distributionManager.paths.dataRoot shouldEqual dataDir
|
||||
distributionManager.paths.config shouldEqual configDir
|
||||
distributionManager.LocallyInstalledDirectories.binDirectory shouldEqual
|
||||
|
@ -2,9 +2,8 @@ package org.enso.launcher.installation
|
||||
|
||||
import java.nio.file.{Files, Path}
|
||||
|
||||
import org.enso.launcher.{FileSystem, NativeTest, WithTemporaryDirectory}
|
||||
import org.enso.launcher.{FileSystem, NativeTest, OS, WithTemporaryDirectory}
|
||||
import org.enso.launcher.FileSystem.PathSyntax
|
||||
import org.enso.launcher.internal.OS
|
||||
|
||||
import scala.io.Source
|
||||
|
||||
@ -51,7 +50,14 @@ class InstallerSpec extends NativeTest with WithTemporaryDirectory {
|
||||
}
|
||||
}
|
||||
|
||||
private def notExistsAfterSomeTime(path: Path, retry: Int = 5): Boolean = {
|
||||
/**
|
||||
* Checks if the file does not exist, retrying `retry` times with a 200ms
|
||||
* delay between retries.
|
||||
*
|
||||
* Useful to check if the file was removed, but the removal action is not
|
||||
* blocking and may take more time.
|
||||
*/
|
||||
def notExistsAfterSomeTime(path: Path, retry: Int = 5): Boolean = {
|
||||
if (Files.notExists(path)) true
|
||||
else if (retry > 0) {
|
||||
Thread.sleep(200)
|
||||
@ -64,7 +70,7 @@ class InstallerSpec extends NativeTest with WithTemporaryDirectory {
|
||||
preparePortableDistribution()
|
||||
runLauncherAt(
|
||||
portableLauncher,
|
||||
Seq("install", "distribution", "--auto-confirm"),
|
||||
Seq("--auto-confirm", "install", "distribution"),
|
||||
env
|
||||
)
|
||||
|
||||
@ -79,17 +85,34 @@ class InstallerSpec extends NativeTest with WithTemporaryDirectory {
|
||||
readFileContent(config).stripTrailing() shouldEqual "what: ever"
|
||||
|
||||
assert(
|
||||
notExistsAfterSomeTime(portableLauncher),
|
||||
Files.notExists(portableLauncher),
|
||||
"The installer should remove itself."
|
||||
)
|
||||
}
|
||||
|
||||
"not remove old launcher if asked" in {
|
||||
preparePortableDistribution()
|
||||
runLauncherAt(
|
||||
portableLauncher,
|
||||
Seq(
|
||||
"--auto-confirm",
|
||||
"install",
|
||||
"distribution",
|
||||
"--no-remove-old-launcher"
|
||||
),
|
||||
env
|
||||
)
|
||||
|
||||
(installedRoot / "bin" / OS.executableName("enso")).toFile should exist
|
||||
portableLauncher.toFile should exist
|
||||
}
|
||||
|
||||
"move bundles by default" in {
|
||||
preparePortableDistribution()
|
||||
prepareBundles()
|
||||
runLauncherAt(
|
||||
portableLauncher,
|
||||
Seq("install", "distribution", "--auto-confirm"),
|
||||
Seq("--auto-confirm", "install", "distribution"),
|
||||
env
|
||||
)
|
||||
|
||||
@ -114,9 +137,9 @@ class InstallerSpec extends NativeTest with WithTemporaryDirectory {
|
||||
runLauncherAt(
|
||||
portableLauncher,
|
||||
Seq(
|
||||
"--auto-confirm",
|
||||
"install",
|
||||
"distribution",
|
||||
"--auto-confirm",
|
||||
"--bundle-install-mode=copy"
|
||||
),
|
||||
env
|
||||
@ -139,9 +162,9 @@ class InstallerSpec extends NativeTest with WithTemporaryDirectory {
|
||||
runLauncherAt(
|
||||
portableLauncher,
|
||||
Seq(
|
||||
"--auto-confirm",
|
||||
"install",
|
||||
"distribution",
|
||||
"--auto-confirm",
|
||||
"--bundle-install-mode=ignore"
|
||||
),
|
||||
env
|
||||
|
56
lib/scala/cli/src/main/scala/org/enso/cli/ProgressBar.scala
Normal file
56
lib/scala/cli/src/main/scala/org/enso/cli/ProgressBar.scala
Normal file
@ -0,0 +1,56 @@
|
||||
package org.enso.cli
|
||||
|
||||
import java.util.concurrent.LinkedTransferQueue
|
||||
|
||||
import scala.util.Try
|
||||
|
||||
/**
|
||||
* Allows to display a progress bar in a terminal.
|
||||
*/
|
||||
object ProgressBar {
|
||||
|
||||
/**
|
||||
* Displays a progressbar tracking progress of the provided task and waits
|
||||
* for its completion.
|
||||
*
|
||||
* The progress bar is displayed as long as the task is in progress. The
|
||||
* function blocks until the task is completed, and then it returns the
|
||||
* result of the task.
|
||||
*
|
||||
* If the total amount of work is unknown and progress cannot be estimated,
|
||||
* an in-progress animation is shown which is updated on each progress
|
||||
* update.
|
||||
*/
|
||||
def waitWithProgress[A](task: TaskProgress[A]): Try[A] = {
|
||||
val progressBar = new internal.ProgressBar
|
||||
progressBar.start()
|
||||
|
||||
sealed trait Update
|
||||
case class Progress(done: Long, total: Option[Long]) extends Update
|
||||
case class Done(result: Try[A]) extends Update
|
||||
|
||||
val queue = new LinkedTransferQueue[Update]()
|
||||
task.addProgressListener(new ProgressListener[A] {
|
||||
override def progressUpdate(done: Long, total: Option[Long]): Unit =
|
||||
queue.put(Progress(done, total))
|
||||
override def done(result: Try[A]): Unit =
|
||||
queue.put(Done(result))
|
||||
})
|
||||
|
||||
var result: Option[Try[A]] = None
|
||||
while (result.isEmpty) {
|
||||
queue.take() match {
|
||||
case Progress(done, Some(total)) =>
|
||||
progressBar.updateProgress(100.0f * done / total)
|
||||
case Progress(_, None) =>
|
||||
progressBar.showUnknownProgress()
|
||||
case Done(incomingResult) =>
|
||||
progressBar.hide()
|
||||
|
||||
result = Some(incomingResult)
|
||||
}
|
||||
}
|
||||
|
||||
result.get
|
||||
}
|
||||
}
|
180
lib/scala/cli/src/main/scala/org/enso/cli/TaskProgress.scala
Normal file
180
lib/scala/cli/src/main/scala/org/enso/cli/TaskProgress.scala
Normal file
@ -0,0 +1,180 @@
|
||||
package org.enso.cli
|
||||
|
||||
import java.util.concurrent.LinkedTransferQueue
|
||||
|
||||
import scala.util.{Failure, Try}
|
||||
|
||||
/**
|
||||
* Clients can implement this trait to get progress updates.
|
||||
*/
|
||||
trait ProgressListener[A] {
|
||||
def progressUpdate(done: Long, total: Option[Long]): Unit
|
||||
def done(result: Try[A]): Unit
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a long-running background task.
|
||||
*/
|
||||
trait TaskProgress[A] {
|
||||
|
||||
/**
|
||||
* Adds a progress listener to this task.
|
||||
*
|
||||
* Even if the task is already finished, the [[ProgressListener.done]]
|
||||
* method should be fired with the result. This way, `done` is fired
|
||||
* exactly once for each attached listener. There are no guarantees on how
|
||||
* often [[ProgressListener.progressUpdate]] is called.
|
||||
*/
|
||||
def addProgressListener(listener: ProgressListener[A]): Unit
|
||||
|
||||
/**
|
||||
* Blocks and waits for the completion of the task.
|
||||
*
|
||||
* Optionally displays a progress bar in the terminal. Returns a [[Try]]
|
||||
* value that wraps the result.
|
||||
*
|
||||
* @param showProgress whether or not to show a progress bar while waiting
|
||||
*/
|
||||
def waitForResult(showProgress: Boolean = false): Try[A] =
|
||||
if (showProgress) ProgressBar.waitWithProgress(this)
|
||||
else TaskProgress.waitForTask(this)
|
||||
|
||||
/**
|
||||
* Alters the task by transforming its result with a function `f` that may
|
||||
* fail.
|
||||
*
|
||||
* The progress of applying `f` is not monitored - it is meant for functions
|
||||
* that do not take a very long time in comparison with the base task.
|
||||
*
|
||||
* @param f the function that can transform the original result
|
||||
* @tparam B the type that `f` returns wrapped in a [[Try]]
|
||||
* @return a new [[TaskProgress]] that will succeed if the original one
|
||||
* succeeded and the transformation succeeded too
|
||||
*/
|
||||
def flatMap[B](f: A => Try[B]): TaskProgress[B] =
|
||||
new MappedTask(this, f)
|
||||
|
||||
/**
|
||||
* Alters the task by transforming its result with a function `f`.
|
||||
*
|
||||
* The progress of applying `f` is not monitored - it is meant for functions
|
||||
* that do not take a very long time in comparison with the base task.
|
||||
*
|
||||
* If an exception is thrown by `f`, the altered task returns a [[Failure]]
|
||||
* with that exception.
|
||||
*
|
||||
* @param f the function that can transform the original result
|
||||
* @tparam B resulting type of `f`
|
||||
* @return a new [[TaskProgress]] that will succeed if the original one
|
||||
* succeeded and the transformation succeeded too
|
||||
*/
|
||||
def map[B](f: A => B): TaskProgress[B] = flatMap(a => Try(f(a)))
|
||||
}
|
||||
|
||||
object TaskProgress {
|
||||
|
||||
/**
|
||||
* Creates a task that fails immediately.
|
||||
*
|
||||
* Useful for reporting early errors (before a background thread has even
|
||||
* been started for the task).
|
||||
*
|
||||
* @param throwable the error to complete the task with
|
||||
* @tparam A type of the task that is failed
|
||||
*/
|
||||
def immediateFailure[A](throwable: Throwable): TaskProgress[A] =
|
||||
new TaskProgress[A] {
|
||||
override def addProgressListener(
|
||||
listener: ProgressListener[A]
|
||||
): Unit = {
|
||||
listener.done(Failure(throwable))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Blocks and waits for the task to complete.
|
||||
*/
|
||||
def waitForTask[A](task: TaskProgress[A]): Try[A] = {
|
||||
val queue = new LinkedTransferQueue[Try[A]]()
|
||||
task.addProgressListener(new ProgressListener[A] {
|
||||
override def progressUpdate(done: Long, total: Option[Long]): Unit = {}
|
||||
override def done(result: Try[A]): Unit =
|
||||
queue.put(result)
|
||||
})
|
||||
|
||||
queue.take()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms the result of the `source` task with `f`.
|
||||
*
|
||||
* Used internally by [[TaskProgress.flatMap]].
|
||||
*/
|
||||
private class MappedTask[A, B](source: TaskProgress[A], f: A => Try[B])
|
||||
extends TaskProgress[B] {
|
||||
override def addProgressListener(
|
||||
listener: ProgressListener[B]
|
||||
): Unit =
|
||||
source.addProgressListener(new ProgressListener[A] {
|
||||
override def progressUpdate(done: Long, total: Option[Long]): Unit =
|
||||
listener.progressUpdate(done, total)
|
||||
override def done(result: Try[A]): Unit =
|
||||
listener.done(result.flatMap(f))
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* A simple implementation of [[TaskProgress]] that can be used to report
|
||||
* progress updates and mark completion of a task.
|
||||
*/
|
||||
class TaskProgressImplementation[A] extends TaskProgress[A] {
|
||||
@volatile private var listeners: List[ProgressListener[A]] = Nil
|
||||
private var result: Option[Try[A]] = None
|
||||
|
||||
override def addProgressListener(
|
||||
listener: ProgressListener[A]
|
||||
): Unit = {
|
||||
this.synchronized {
|
||||
result match {
|
||||
case Some(value) =>
|
||||
listener.done(value)
|
||||
case None =>
|
||||
}
|
||||
|
||||
listeners ::= listener
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks the completion of this task.
|
||||
*
|
||||
* All registered listeners are immediately notified. Any listeners added
|
||||
* later will also be notified as soon as they are added.
|
||||
*
|
||||
* Can be called only once per task.
|
||||
* @param result the result to complete the task with
|
||||
*/
|
||||
def setComplete(result: Try[A]): Unit = {
|
||||
this.synchronized {
|
||||
if (this.result.isDefined) {
|
||||
throw new IllegalStateException(
|
||||
"A task has been completed more than once."
|
||||
)
|
||||
}
|
||||
|
||||
this.result = Some(result)
|
||||
listeners.foreach(_.done(result))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Report a progress update to all registered listeners.
|
||||
*
|
||||
* This operation is not synchronized, as it is not a problem if a just added
|
||||
* listener does not get the latest progress update.
|
||||
*/
|
||||
def reportProgress(done: Long, total: Option[Long]): Unit = {
|
||||
listeners.foreach(_.progressUpdate(done, total))
|
||||
}
|
||||
}
|
@ -0,0 +1,72 @@
|
||||
package org.enso.cli.internal
|
||||
|
||||
/**
|
||||
* Allows to display a progress bar in a terminal.
|
||||
*/
|
||||
class ProgressBar {
|
||||
|
||||
/**
|
||||
* Begins drawing the progressbar.
|
||||
*/
|
||||
def start(): Unit = {
|
||||
drawProgressBar(0, "")
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the progressbar with a percentage.
|
||||
*/
|
||||
def updateProgress(percentage: Float): Unit = {
|
||||
drawProgressBar(
|
||||
(percentage / 100.0f * totalStates).floor.toInt,
|
||||
s"${percentage.toInt}%"
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the progressbar.
|
||||
*/
|
||||
def hide(): Unit = {
|
||||
print("\r" + " " * (paddingLength + 2) + "\r")
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays a next step of animation indicating progress of a task with an
|
||||
* unknown total.
|
||||
*/
|
||||
def showUnknownProgress(): Unit = {
|
||||
state += 1
|
||||
val pos = state % progressWidth
|
||||
val prefix = " " * pos
|
||||
val suffix = " " * (progressWidth - pos - 1)
|
||||
val bar = s" [$prefix?$suffix]\r"
|
||||
print(bar)
|
||||
}
|
||||
|
||||
private var state = 0
|
||||
private var longestComment: Int = 0
|
||||
def paddingLength: Int = progressWidth + 4 + longestComment
|
||||
|
||||
private val progressWidth = 20
|
||||
private val progressStates = Seq(".", "#")
|
||||
private val totalStates = progressWidth * progressStates.length
|
||||
|
||||
private def drawProgressBar(state: Int, comment: String): Unit = {
|
||||
longestComment = Seq(longestComment, comment.length).max
|
||||
|
||||
val stateClamped = Seq(Seq(state, 0).max, totalStates).min
|
||||
val full =
|
||||
progressStates.last * (stateClamped / progressStates.length)
|
||||
val partial = {
|
||||
val idx = stateClamped % progressStates.length
|
||||
if (idx > 0) {
|
||||
progressStates(idx - 1)
|
||||
} else ""
|
||||
}
|
||||
|
||||
val bar = full + partial
|
||||
val rest = " " * (progressWidth - bar.length)
|
||||
val line = s"[$bar$rest] $comment"
|
||||
val padding = " " * (paddingLength - line.length)
|
||||
print(" " + line + padding + "\r")
|
||||
}
|
||||
}
|
@ -3,7 +3,6 @@ import java.io.IOException
|
||||
import sbt.Keys._
|
||||
import sbt._
|
||||
import sbt.internal.util.ManagedLogger
|
||||
import sbt.util.FilesInfo
|
||||
|
||||
import scala.sys.process._
|
||||
|
||||
@ -51,8 +50,8 @@ object GenerateFlatbuffers {
|
||||
|
||||
val projectName = name.value
|
||||
log.info(
|
||||
"*** Flatbuffers code generation generated " +
|
||||
s"${generatedSources.size} files in project $projectName"
|
||||
s"Flatbuffers code generation generated ${generatedSources.size} " +
|
||||
s"files in project $projectName."
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -1,16 +1,37 @@
|
||||
import java.io.File
|
||||
import java.nio.file.Path
|
||||
|
||||
import sbt.{File, _}
|
||||
import sbt.{Def, File, _}
|
||||
import sbt.Keys._
|
||||
import sbt.internal.util.ManagedLogger
|
||||
|
||||
import scala.sys.process._
|
||||
|
||||
object NativeImage {
|
||||
def buildNativeImage(staticOnLinux: Boolean): Def.Initialize[Task[Unit]] =
|
||||
|
||||
/**
|
||||
* Specifies whether the build executable should include debug symbols. Should
|
||||
* be set to false for production builds. May work only on Linux.
|
||||
*/
|
||||
private val includeDebugInfo: Boolean = false
|
||||
|
||||
/**
|
||||
* Creates a task that builds a native image for the current project.
|
||||
*
|
||||
* @param artifactName name of the artifact to create
|
||||
* @param staticOnLinux specifies whether to link statically (applies only
|
||||
* on Linux)
|
||||
*/
|
||||
def buildNativeImage(
|
||||
artifactName: String,
|
||||
staticOnLinux: Boolean,
|
||||
additionalOptions: Seq[String] = Seq.empty
|
||||
): Def.Initialize[Task[Unit]] =
|
||||
Def
|
||||
.task {
|
||||
val log = state.value.log
|
||||
val javaHome = System.getProperty("java.home")
|
||||
val log = state.value.log
|
||||
val javaHome = System.getProperty("java.home")
|
||||
val subProjectRoot = baseDirectory.value
|
||||
val nativeImagePath =
|
||||
if (isWindows)
|
||||
s"$javaHome\\bin\\native-image.cmd"
|
||||
@ -18,17 +39,6 @@ object NativeImage {
|
||||
val classPath =
|
||||
(Runtime / fullClasspath).value.files.mkString(File.pathSeparator)
|
||||
|
||||
val additionalParameters =
|
||||
if (staticOnLinux && isLinux)
|
||||
"--static"
|
||||
else ""
|
||||
val resourcesGlobOpt = "-H:IncludeResources=.*Main.enso$"
|
||||
|
||||
val cmd =
|
||||
s"$nativeImagePath $additionalParameters $resourcesGlobOpt " +
|
||||
s"--no-fallback --initialize-at-build-time" +
|
||||
s" -cp $classPath ${(Compile / mainClass).value.get} enso"
|
||||
|
||||
if (!file(nativeImagePath).exists()) {
|
||||
log.error("Native Image component not found in the JVM distribution.")
|
||||
log.error(
|
||||
@ -40,6 +50,43 @@ object NativeImage {
|
||||
)
|
||||
}
|
||||
|
||||
val debugParameters =
|
||||
if (includeDebugInfo) "-H:GenerateDebugInfo=1" else ""
|
||||
|
||||
val staticParameters =
|
||||
if (staticOnLinux && isLinux) {
|
||||
// Note [Static Build On Linux]
|
||||
val buildCache =
|
||||
subProjectRoot / "build-cache"
|
||||
val path = ensureMuslIsInstalled(buildCache, log)
|
||||
s"--static -H:UseMuslC=$path"
|
||||
} else ""
|
||||
|
||||
val resourcesGlobOpt = "-H:IncludeResources=.*Main.enso$"
|
||||
|
||||
val configLocation =
|
||||
subProjectRoot / "native-image-config"
|
||||
val configs =
|
||||
if (configLocation.exists()) {
|
||||
val path = configLocation.toPath.toAbsolutePath
|
||||
log.debug(s"Picking up Native Image configuration from `$path`.")
|
||||
s"-H:ConfigurationFileDirectories=$path"
|
||||
} else {
|
||||
log.debug(
|
||||
"No Native Image configuration found, proceeding without it."
|
||||
)
|
||||
""
|
||||
}
|
||||
|
||||
val cmd =
|
||||
s"$nativeImagePath $staticParameters $debugParameters " +
|
||||
s"$resourcesGlobOpt $configs " +
|
||||
s"--no-fallback --initialize-at-build-time " +
|
||||
s"${additionalOptions.mkString(" ")} " +
|
||||
s"-cp $classPath ${(Compile / mainClass).value.get} enso"
|
||||
|
||||
log.debug(cmd)
|
||||
|
||||
if (cmd.! != 0) {
|
||||
log.error("Native Image build failed.")
|
||||
throw new RuntimeException("Native Image build failed")
|
||||
@ -49,10 +96,20 @@ object NativeImage {
|
||||
}
|
||||
.dependsOn(Compile / compile)
|
||||
|
||||
/**
|
||||
* Creates a task which watches for changes of any compiled files or
|
||||
* dependencies and triggers a rebuild if and only if there are any changes.
|
||||
*
|
||||
* @param actualBuild reference to the task doing the actual Native Image
|
||||
* build, usually one returned by [[buildNativeImage]]
|
||||
* @param artifactName name of the artifact that is expected to be created
|
||||
* by the native image build
|
||||
* @return
|
||||
*/
|
||||
def incrementalNativeImageBuild(
|
||||
actualBuild: TaskKey[Unit],
|
||||
artifactName: String
|
||||
) =
|
||||
): Def.Initialize[Task[Unit]] =
|
||||
Def.taskDyn {
|
||||
def rebuild(reason: String) = {
|
||||
streams.value.log.info(
|
||||
@ -92,4 +149,87 @@ object NativeImage {
|
||||
private 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"
|
||||
|
||||
/**
|
||||
* Ensures that the `musl` bundle is installed.
|
||||
*
|
||||
* Checks for existence of its directory and if it does not exist, downloads
|
||||
* and extracts the bundle. `musl` is needed for static builds on Linux.
|
||||
*
|
||||
* @param buildCache build-cache directory for the current project
|
||||
* @param log a logger instance
|
||||
* @return path to the `musl` bundle that can be passed to the Native Image
|
||||
* as a parameter
|
||||
*/
|
||||
private def ensureMuslIsInstalled(
|
||||
buildCache: File,
|
||||
log: ManagedLogger
|
||||
): Path = {
|
||||
val muslRoot = buildCache / "musl-1.2.0"
|
||||
val bundleLocation = muslRoot / "bundle"
|
||||
if (!bundleLocation.exists()) {
|
||||
log.info(
|
||||
"`musl` is required for a static build, but it is not installed for " +
|
||||
"this subproject."
|
||||
)
|
||||
try {
|
||||
log.info("A `musl` bundle will be downloaded.")
|
||||
buildCache.mkdirs()
|
||||
val bundle = buildCache / "musl-bundle.tar.gz"
|
||||
|
||||
val downloadExitCode = (url(muslBundleUrl) #> bundle).!
|
||||
if (downloadExitCode != 0) {
|
||||
log.error("Cannot download `musl` bundle.")
|
||||
throw new RuntimeException(s"Cannot download `$muslBundleUrl`.")
|
||||
}
|
||||
|
||||
muslRoot.mkdirs()
|
||||
val tarExitCode = Seq(
|
||||
"tar",
|
||||
"xf",
|
||||
bundle.toPath.toAbsolutePath.toString,
|
||||
"-C",
|
||||
muslRoot.toPath.toAbsolutePath.toString
|
||||
).!
|
||||
if (tarExitCode != 0) {
|
||||
log.error(
|
||||
"An error occurred when extracting the `musl` library bundle."
|
||||
)
|
||||
throw new RuntimeException(s"Cannot extract $bundle.")
|
||||
}
|
||||
|
||||
log.info("Installed `musl`.")
|
||||
} catch {
|
||||
case e: Exception =>
|
||||
throw new RuntimeException(
|
||||
"`musl` installation failed. Cannot proceed with a static " +
|
||||
"Native Image build.",
|
||||
e
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
bundleLocation.toPath.toAbsolutePath
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/* Note [Static Build On Linux]
|
||||
* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
* The default `glibc` contains a bug that would cause crashes when downloading
|
||||
* files form the internet, which is a crucial Launcher functionality. Instead,
|
||||
* `musl` is suggested by Graal as an alternative libc. The sbt task
|
||||
* automatically downloads a bundle containing all requirements for a static
|
||||
* build with `musl`.
|
||||
*
|
||||
* Currently, to use `musl`, the `-H:UseMuslC=/path/to/musl/bundle` option has
|
||||
* to be added to the build. In the future, a `--libc=musl` option may be
|
||||
* preferred instead, as described at
|
||||
* https://github.com/oracle/graal/blob/master/substratevm/STATIC-IMAGES.md
|
||||
* or even `musl` may be included by default. This task may thus need an update
|
||||
* when moving to a newer version of Graal.
|
||||
*/
|
||||
|
Loading…
Reference in New Issue
Block a user