From 8336a97931c1cc893e5ed7cc926b74326e377c8c Mon Sep 17 00:00:00 2001 From: Dmitry Bushev Date: Tue, 27 Jul 2021 19:13:14 +0300 Subject: [PATCH] Add Support for Project Templates (#1902) Add an ability to create a project from a template --- RELEASES.md | 5 + .../protocol-project-manager.md | 3 + .../scala/org/enso/launcher/Launcher.scala | 2 + .../launcher/cli/LauncherApplication.scala | 10 ++ .../components/LauncherRunnerSpec.scala | 2 + .../src/main/scala/org/enso/runner/Main.scala | 38 ++++++-- .../runtime/util/TruffleFileSystem.java | 14 ++- .../resources/{ => default/src}/Main.enso | 0 .../pkg/src/main/resources/example/hello.txt | 1 + .../src/main/resources/example/src/Main.enso | 5 + .../org/enso/filesystem/FileSystem.scala | 35 ++++++- .../src/main/scala/org/enso/pkg/Package.scala | 91 +++++++++++++++---- .../main/scala/org/enso/pkg/Template.scala | 30 ++++++ .../protocol/ProjectManagementApi.scala | 2 + .../requesthandler/ProjectCreateHandler.scala | 1 + .../service/ProjectCreationService.scala | 14 ++- .../service/ProjectCreationServiceApi.scala | 2 + .../service/ProjectService.scala | 46 +++++++++- .../service/ProjectServiceApi.scala | 4 +- .../projectmanager/ProjectManagementOps.scala | 5 + .../protocol/ProjectManagementApiSpec.scala | 54 ++++++++++- .../protocol/ProjectOpenSpecBase.scala | 18 ++-- .../runtimeversionmanager/runner/Runner.scala | 5 +- 23 files changed, 340 insertions(+), 47 deletions(-) rename lib/scala/pkg/src/main/resources/{ => default/src}/Main.enso (100%) create mode 100644 lib/scala/pkg/src/main/resources/example/hello.txt create mode 100644 lib/scala/pkg/src/main/resources/example/src/Main.enso create mode 100644 lib/scala/pkg/src/main/scala/org/enso/pkg/Template.scala diff --git a/RELEASES.md b/RELEASES.md index 22c9094e565..be0de426777 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -5,6 +5,11 @@ - Added support for documenting modules directly ([#1900](https://github.com/enso-org/enso/pull/1900)). +## Tooling + +- Added support for creating projects from a template + ([#1902](https://github.com/enso-org/enso/pull/1902)). + # Enso 0.2.16 (2021-07-23) ## Interpreter/Runtime diff --git a/docs/language-server/protocol-project-manager.md b/docs/language-server/protocol-project-manager.md index 217ec9482e2..85c7aa71830 100644 --- a/docs/language-server/protocol-project-manager.md +++ b/docs/language-server/protocol-project-manager.md @@ -336,6 +336,9 @@ interface ProjectCreateRequest { /** Name of the project to create. */ name: String; + /** The name of the project template to create. */ + projectTemplate?: String; + /** * Enso Engine version to use for the project. * diff --git a/engine/launcher/src/main/scala/org/enso/launcher/Launcher.scala b/engine/launcher/src/main/scala/org/enso/launcher/Launcher.scala index 36d1a4f3c06..70b3576a2fe 100644 --- a/engine/launcher/src/main/scala/org/enso/launcher/Launcher.scala +++ b/engine/launcher/src/main/scala/org/enso/launcher/Launcher.scala @@ -72,6 +72,7 @@ case class Launcher(cliOptions: GlobalCLIOptions) { */ def newProject( name: String, + projectTemplate: Option[String], path: Option[Path], versionOverride: Option[SemVer], useSystemJVM: Boolean, @@ -90,6 +91,7 @@ case class Launcher(cliOptions: GlobalCLIOptions) { path = actualPath, name = name, engineVersion = version, + projectTemplate = projectTemplate, authorName = globalConfig.authorName, authorEmail = globalConfig.authorEmail, additionalArguments = additionalArguments diff --git a/engine/launcher/src/main/scala/org/enso/launcher/cli/LauncherApplication.scala b/engine/launcher/src/main/scala/org/enso/launcher/cli/LauncherApplication.scala index 85b5b3cd046..b8c42db1fe4 100644 --- a/engine/launcher/src/main/scala/org/enso/launcher/cli/LauncherApplication.scala +++ b/engine/launcher/src/main/scala/org/enso/launcher/cli/LauncherApplication.scala @@ -57,11 +57,19 @@ object LauncherApplication { "PATH specifies where to create the project. If it is not specified, " + "a directory called PROJECT-NAME is created in the current directory." ) + def projectTemplate = { + Opts.optionalParameter[String]( + "new-project-template", + "TEMPLATE-NAME", + "Specifies a project template when creating a project." + ) + } val additionalArgs = Opts.additionalArguments() ( nameOpt, pathOpt, + projectTemplate, versionOverride, systemJVMOverride, jvmOpts, @@ -70,6 +78,7 @@ object LauncherApplication { ( name, path, + template, versionOverride, systemJVMOverride, jvmOpts, @@ -78,6 +87,7 @@ object LauncherApplication { Launcher(config).newProject( name = name, path = path, + projectTemplate = template, versionOverride = versionOverride, useSystemJVM = systemJVMOverride, jvmOpts = jvmOpts, diff --git a/engine/launcher/src/test/scala/org/enso/launcher/components/LauncherRunnerSpec.scala b/engine/launcher/src/test/scala/org/enso/launcher/components/LauncherRunnerSpec.scala index 18d0ab35e68..57569e5aae3 100644 --- a/engine/launcher/src/test/scala/org/enso/launcher/components/LauncherRunnerSpec.scala +++ b/engine/launcher/src/test/scala/org/enso/launcher/components/LauncherRunnerSpec.scala @@ -124,6 +124,7 @@ class LauncherRunnerSpec extends RuntimeVersionManagerTest { path = projectPath, name = "ProjectName", engineVersion = defaultEngineVersion, + projectTemplate = None, authorName = Some(authorName), authorEmail = Some(authorEmail), additionalArguments = Seq(additionalArgument) @@ -151,6 +152,7 @@ class LauncherRunnerSpec extends RuntimeVersionManagerTest { path = projectPath, name = "ProjectName2", engineVersion = nightlyVersion, + projectTemplate = None, authorName = None, authorEmail = None, additionalArguments = Seq() diff --git a/engine/runner/src/main/scala/org/enso/runner/Main.scala b/engine/runner/src/main/scala/org/enso/runner/Main.scala index b3dbc57d875..51777674eb7 100644 --- a/engine/runner/src/main/scala/org/enso/runner/Main.scala +++ b/engine/runner/src/main/scala/org/enso/runner/Main.scala @@ -8,13 +8,13 @@ import org.enso.editions.DefaultEdition import org.enso.languageserver.boot import org.enso.languageserver.boot.LanguageServerConfig import org.enso.loggingservice.LogLevel -import org.enso.pkg.{Contact, PackageManager} +import org.enso.pkg.{Contact, PackageManager, Template} import org.enso.polyglot.{LanguageInfo, Module, PolyglotContext} import org.enso.version.VersionDescription import org.graalvm.polyglot.PolyglotException - import java.io.File import java.util.UUID + import scala.Console.err import scala.jdk.CollectionConverters._ import scala.util.Try @@ -26,6 +26,7 @@ object Main { private val HELP_OPTION = "help" private val NEW_OPTION = "new" private val PROJECT_NAME_OPTION = "new-project-name" + private val PROJECT_TEMPLATE_OPTION = "new-project-template" private val PROJECT_AUTHOR_NAME_OPTION = "new-project-author-name" private val PROJECT_AUTHOR_EMAIL_OPTION = "new-project-author-email" private val REPL_OPTION = "repl" @@ -87,6 +88,15 @@ object Main { s"Specifies a project name when creating a project using --$NEW_OPTION." ) .build + val newProjectTemplateOpt = CliOption.builder + .hasArg(true) + .numberOfArgs(1) + .argName("name") + .longOpt(PROJECT_TEMPLATE_OPTION) + .desc( + s"Specifies a project template when creating a project using --$NEW_OPTION." + ) + .build val newProjectAuthorNameOpt = CliOption.builder .hasArg(true) .numberOfArgs(1) @@ -202,6 +212,7 @@ object Main { .addOption(docs) .addOption(newOpt) .addOption(newProjectNameOpt) + .addOption(newProjectTemplateOpt) .addOption(newProjectAuthorNameOpt) .addOption(newProjectAuthorEmailOpt) .addOption(lsOption) @@ -248,12 +259,14 @@ object Main { * * @param path root path of the newly created project * @param nameOption specifies the name of the created project + * @param templateOption specifies the template of the created project * @param authorName if set, sets the name of the author and maintainer * @param authorEmail if set, sets the email of the author and maintainer */ private def createNew( path: String, nameOption: Option[String], + templateOption: Option[String], authorName: Option[String], authorEmail: Option[String] ): Unit = { @@ -270,12 +283,22 @@ object Main { s"Creating a new project $name based on edition [$baseEdition]." ) } + + val template = templateOption + .map { name => + Template.fromString(name).getOrElse { + logger.error(s"Unknown project template name: '$name'.") + exitFail() + } + } + PackageManager.Default.create( root = root, name = name, edition = Some(edition), authors = authors, - maintainers = authors + maintainers = authors, + template = template.getOrElse(Template.Default) ) exitSuccess() } @@ -629,10 +652,11 @@ object Main { if (line.hasOption(NEW_OPTION)) { createNew( - path = line.getOptionValue(NEW_OPTION), - nameOption = Option(line.getOptionValue(PROJECT_NAME_OPTION)), - authorName = Option(line.getOptionValue(PROJECT_AUTHOR_NAME_OPTION)), - authorEmail = Option(line.getOptionValue(PROJECT_AUTHOR_EMAIL_OPTION)) + path = line.getOptionValue(NEW_OPTION), + nameOption = Option(line.getOptionValue(PROJECT_NAME_OPTION)), + authorName = Option(line.getOptionValue(PROJECT_AUTHOR_NAME_OPTION)), + authorEmail = Option(line.getOptionValue(PROJECT_AUTHOR_EMAIL_OPTION)), + templateOption = Option(line.getOptionValue(PROJECT_TEMPLATE_OPTION)) ) } diff --git a/engine/runtime/src/main/java/org/enso/interpreter/runtime/util/TruffleFileSystem.java b/engine/runtime/src/main/java/org/enso/interpreter/runtime/util/TruffleFileSystem.java index f93dfc06eda..6f99d8d42fb 100644 --- a/engine/runtime/src/main/java/org/enso/interpreter/runtime/util/TruffleFileSystem.java +++ b/engine/runtime/src/main/java/org/enso/interpreter/runtime/util/TruffleFileSystem.java @@ -3,9 +3,7 @@ package org.enso.interpreter.runtime.util; import com.oracle.truffle.api.TruffleFile; import org.enso.filesystem.FileSystem; -import java.io.BufferedReader; -import java.io.BufferedWriter; -import java.io.IOException; +import java.io.*; import java.nio.file.FileVisitResult; import java.nio.file.SimpleFileVisitor; import java.nio.file.attribute.BasicFileAttributes; @@ -53,6 +51,16 @@ public class TruffleFileSystem implements FileSystem { return file.getName(); } + @Override + public InputStream newInputStream(TruffleFile file) throws IOException { + return file.newInputStream(); + } + + @Override + public OutputStream newOutputStream(TruffleFile file) throws IOException { + return file.newOutputStream(); + } + @Override public BufferedWriter newBufferedWriter(TruffleFile file) throws IOException { return file.newBufferedWriter(); diff --git a/lib/scala/pkg/src/main/resources/Main.enso b/lib/scala/pkg/src/main/resources/default/src/Main.enso similarity index 100% rename from lib/scala/pkg/src/main/resources/Main.enso rename to lib/scala/pkg/src/main/resources/default/src/Main.enso diff --git a/lib/scala/pkg/src/main/resources/example/hello.txt b/lib/scala/pkg/src/main/resources/example/hello.txt new file mode 100644 index 00000000000..980a0d5f19a --- /dev/null +++ b/lib/scala/pkg/src/main/resources/example/hello.txt @@ -0,0 +1 @@ +Hello World! diff --git a/lib/scala/pkg/src/main/resources/example/src/Main.enso b/lib/scala/pkg/src/main/resources/example/src/Main.enso new file mode 100644 index 00000000000..083219f4f0e --- /dev/null +++ b/lib/scala/pkg/src/main/resources/example/src/Main.enso @@ -0,0 +1,5 @@ +from Standard.Base import all + +main = + operator1 = Enso_Project.root / "hello.txt" + File.read operator1 diff --git a/lib/scala/pkg/src/main/scala/org/enso/filesystem/FileSystem.scala b/lib/scala/pkg/src/main/scala/org/enso/filesystem/FileSystem.scala index ddbdf14b6dd..b909cf8fbdb 100644 --- a/lib/scala/pkg/src/main/scala/org/enso/filesystem/FileSystem.scala +++ b/lib/scala/pkg/src/main/scala/org/enso/filesystem/FileSystem.scala @@ -1,7 +1,14 @@ package org.enso.filesystem import scala.jdk.CollectionConverters._ -import java.io.{BufferedReader, BufferedWriter, File, IOException} +import java.io.{ + BufferedReader, + BufferedWriter, + File, + IOException, + InputStream, + OutputStream +} import java.nio.file.Files import java.nio.file.attribute.{BasicFileAttributes, FileTime} import java.util.stream @@ -64,6 +71,22 @@ trait FileSystem[F] { */ def getName(file: F): String + /** Creates a new input stream for the given file. + * + * @param file the file to open. + * @return an input stream for `file`. + */ + @throws[IOException] + def newInputStream(file: F): InputStream + + /** Creates a new output stream for the given file. + * + * @param file the file to open. + * @return an output stream for `file`. + */ + @throws[IOException] + def newOutputStream(file: F): OutputStream + /** Creates a new buffered writer for the given file. * * @param file the file to open. @@ -140,6 +163,10 @@ object FileSystem { def getName: String = fs.getName(file) + def newInputStream: InputStream = fs.newInputStream(file) + + def newOutputStream: OutputStream = fs.newOutputStream(file) + def newBufferedWriter: BufferedWriter = fs.newBufferedWriter(file) def newBufferedReader: BufferedReader = fs.newBufferedReader(file) @@ -175,6 +202,12 @@ object FileSystem { override def getName(file: File): String = file.getName + override def newInputStream(file: File): InputStream = + Files.newInputStream(file.toPath) + + override def newOutputStream(file: File): OutputStream = + Files.newOutputStream(file.toPath) + override def newBufferedWriter(file: File): BufferedWriter = Files.newBufferedWriter(file.toPath) diff --git a/lib/scala/pkg/src/main/scala/org/enso/pkg/Package.scala b/lib/scala/pkg/src/main/scala/org/enso/pkg/Package.scala index 8b4f2a5e2b7..d4ea2363a1f 100644 --- a/lib/scala/pkg/src/main/scala/org/enso/pkg/Package.scala +++ b/lib/scala/pkg/src/main/scala/org/enso/pkg/Package.scala @@ -4,9 +4,9 @@ import cats.Show import org.enso.editions.{Editions, LibraryName} import org.enso.filesystem.FileSystem import org.enso.pkg.validation.NameValidation +import java.io.{File, InputStream, OutputStream} +import java.net.URI -import java.io.File -import scala.io.Source import scala.jdk.CollectionConverters._ import scala.util.{Failure, Try, Using} @@ -48,13 +48,14 @@ case class Package[F]( /** Stores the package metadata on the hard drive. If the package does not exist, * creates the required directory structure. */ - def save(): Try[Unit] = for { - _ <- Try { - if (!root.exists) createDirectories() - if (!sourceDir.exists) createSourceDir() - } - _ <- saveConfig() - } yield () + def save(): Try[Unit] = + for { + _ <- Try { + if (!root.exists) createDirectories() + if (!sourceDir.exists) createSourceDir() + } + _ <- saveConfig() + } yield () /** Creates the package directory structure. */ @@ -82,19 +83,14 @@ case class Package[F]( */ def updateConfig(update: Config => Config): Package[F] = { val newPkg = copy(config = update(config)) - newPkg.save() + newPkg.saveConfig() newPkg } - /** Creates the sources directory and populates it with a dummy Main file. + /** Creates the sources directory. */ def createSourceDir(): Unit = { Try(sourceDir.createDirectories()).getOrElse(throw CouldNotCreateDirectory) - val mainCodeSrc = Source.fromResource(Package.mainFileName) - val writer = sourceDir.getChild(Package.mainFileName).newBufferedWriter - writer.write(mainCodeSrc.mkString) - writer.close() - mainCodeSrc.close() } /** Saves the config metadata into the package configuration file. @@ -185,10 +181,12 @@ class PackageManager[F](implicit val fileSystem: FileSystem[F]) { */ def create( root: F, - config: Config + config: Config, + template: Template ): Package[F] = { val pkg = Package(root, config, fileSystem) pkg.save() + copyResources(pkg, template) pkg } @@ -197,6 +195,7 @@ class PackageManager[F](implicit val fileSystem: FileSystem[F]) { * @param root the root location of the package. * @param name the name for the new package. * @param version version of the newly-created package. + * @param template the template for the new package. * @param edition the edition to use for the project; if not specified, it * will not specify any, meaning that the current default one * will be used @@ -207,6 +206,7 @@ class PackageManager[F](implicit val fileSystem: FileSystem[F]) { name: String, namespace: String = "local", version: String = "0.0.1", + template: Template = Template.Default, edition: Option[Editions.RawEdition] = None, authors: List[Contact] = List(), maintainers: List[Contact] = List(), @@ -222,7 +222,7 @@ class PackageManager[F](implicit val fileSystem: FileSystem[F]) { preferLocalLibraries = true, maintainers = maintainers ) - create(root, config) + create(root, config, template) } /** Tries to parse package structure from a given root location. @@ -302,6 +302,51 @@ class PackageManager[F](implicit val fileSystem: FileSystem[F]) { val dirname = file.getName NameValidation.normalizeName(dirname) } + + /** Copy the template resources to the package. + * + * @param template the template to copy the resources from + * @param pkg the package to copy the resources to + */ + private def copyResources(pkg: Package[F], template: Template): Unit = + template match { + case Template.Default => + val mainEnsoPath = new URI(s"/default/src/${Package.mainFileName}") + + copyResource( + mainEnsoPath, + pkg.sourceDir.getChild(Package.mainFileName) + ) + + case Template.Example => + val helloTxtPath = new URI("/example/hello.txt") + val mainEnsoPath = new URI(s"/example/src/${Package.mainFileName}") + + copyResource( + helloTxtPath, + pkg.root.getChild("hello.txt") + ) + copyResource( + mainEnsoPath, + pkg.sourceDir.getChild(Package.mainFileName) + ) + } + + /** Copy the resource to provided resource. + * + * @param from the source + * @param to the destination + */ + private def copyResource(from: URI, to: F): Unit = { + val fromStream = getClass.getResourceAsStream(from.toString) + val toStream = to.newOutputStream + try PackageManager.copyStream(fromStream, toStream) + finally { + fromStream.close() + toStream.close() + } + } + } object PackageManager { @@ -326,6 +371,16 @@ object PackageManager { /** The error indicating that the project name is invalid. */ case class InvalidNameException(message: String) extends RuntimeException(message) + + private def copyStream(in: InputStream, out: OutputStream): Unit = { + val buffer = Array.ofDim[Byte](4096) + var length = in.read(buffer) + while (length != -1) { + out.write(buffer, 0, length) + length = in.read(buffer) + } + } + } /** A companion object for static methods on the [[Package]] class. diff --git a/lib/scala/pkg/src/main/scala/org/enso/pkg/Template.scala b/lib/scala/pkg/src/main/scala/org/enso/pkg/Template.scala new file mode 100644 index 00000000000..d05fd11184d --- /dev/null +++ b/lib/scala/pkg/src/main/scala/org/enso/pkg/Template.scala @@ -0,0 +1,30 @@ +package org.enso.pkg + +/** Base trait for the project templates. */ +sealed trait Template { + + /** The template name. */ + def name: String +} +object Template { + + /** Create a template from string. + * + * @param template the template name + * @return the template for the provided name + */ + def fromString(template: String): Option[Template] = + allTemplates.find(_.name == template.toLowerCase) + + /** The default project template. */ + case object Default extends Template { + override val name = "default" + } + + /** The example project template. */ + case object Example extends Template { + override val name = "example" + } + + val allTemplates = Seq(Default, Example) +} diff --git a/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/protocol/ProjectManagementApi.scala b/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/protocol/ProjectManagementApi.scala index a96e8f4df78..2a78e0cfbb8 100644 --- a/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/protocol/ProjectManagementApi.scala +++ b/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/protocol/ProjectManagementApi.scala @@ -1,6 +1,7 @@ package org.enso.projectmanager.protocol import java.util.UUID + import io.circe.Json import io.circe.syntax._ import nl.gn0s1s.bump.SemVer @@ -24,6 +25,7 @@ object ProjectManagementApi { case class Params( name: String, version: Option[EnsoVersion], + projectTemplate: Option[String], missingComponentAction: Option[MissingComponentAction] ) diff --git a/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/requesthandler/ProjectCreateHandler.scala b/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/requesthandler/ProjectCreateHandler.scala index d4255a8da7a..8d9c7cd4a9b 100644 --- a/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/requesthandler/ProjectCreateHandler.scala +++ b/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/requesthandler/ProjectCreateHandler.scala @@ -56,6 +56,7 @@ class ProjectCreateHandler[F[+_, +_]: Exec: CovariantFlatMap: ErrorChannel]( progressTracker = self, name = params.name, engineVersion = actualVersion, + projectTemplate = params.projectTemplate, missingComponentAction = missingComponentAction ) } yield ProjectCreate.Result(projectId) diff --git a/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/service/ProjectCreationService.scala b/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/service/ProjectCreationService.scala index 5e99207241d..6c9133a124e 100644 --- a/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/service/ProjectCreationService.scala +++ b/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/service/ProjectCreationService.scala @@ -14,7 +14,6 @@ import org.enso.projectmanager.service.versionmanagement.RuntimeVersionManagerFa import org.enso.projectmanager.versionmanagement.DistributionConfiguration import org.enso.runtimeversionmanager.config.GlobalConfigurationManager import org.enso.runtimeversionmanager.runner.Runner - import java.nio.file.Path /** A service for creating new project structures using the runner of the @@ -35,6 +34,7 @@ class ProjectCreationService[ path: Path, name: String, engineVersion: SemVer, + projectTemplate: Option[String], missingComponentAction: MissingComponentAction ): F[ProjectServiceFailure, Unit] = Sync[F] .blockingOp { @@ -57,7 +57,17 @@ class ProjectCreationService[ ) val settings = - runner.newProject(path, name, engineVersion, None, None, Seq()).get + runner + .newProject( + path, + name, + engineVersion, + projectTemplate, + None, + None, + Seq() + ) + .get val jvmSettings = distributionConfiguration.defaultJVMSettings runner.withCommand(settings, jvmSettings) { command => logger.trace( diff --git a/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/service/ProjectCreationServiceApi.scala b/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/service/ProjectCreationServiceApi.scala index da91a21d7be..d51ada45871 100644 --- a/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/service/ProjectCreationServiceApi.scala +++ b/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/service/ProjectCreationServiceApi.scala @@ -18,6 +18,7 @@ trait ProjectCreationServiceApi[F[+_, +_]] { * @param path path at which to create the project * @param name name of the project * @param engineVersion version of the engine this project is meant for + * @param projectTemplate the name of the project template * @param missingComponentAction specifies how to handle missing components */ def createProject( @@ -25,6 +26,7 @@ trait ProjectCreationServiceApi[F[+_, +_]] { path: Path, name: String, engineVersion: SemVer, + projectTemplate: Option[String], missingComponentAction: MissingComponentAction ): F[ProjectServiceFailure, Unit] } diff --git a/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/service/ProjectService.scala b/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/service/ProjectService.scala index 5b2610d573b..9309e7b9413 100644 --- a/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/service/ProjectService.scala +++ b/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/service/ProjectService.scala @@ -47,7 +47,6 @@ import org.enso.projectmanager.service.config.GlobalConfigServiceApi import org.enso.projectmanager.service.versionmanagement.RuntimeVersionManagerErrorRecoverySyntax._ import org.enso.projectmanager.service.versionmanagement.RuntimeVersionManagerFactory import org.enso.projectmanager.versionmanagement.DistributionConfiguration - import java.util.UUID /** Implementation of business logic for project management. @@ -82,12 +81,19 @@ class ProjectService[ /** @inheritdoc */ override def createUserProject( progressTracker: ActorRef, - name: String, + projectName: String, engineVersion: SemVer, + projectTemplate: Option[String], missingComponentAction: MissingComponentAction ): F[ProjectServiceFailure, UUID] = for { - projectId <- gen.randomUUID() - _ <- log.debug("Creating project [{}, {}].", name, projectId) + projectId <- gen.randomUUID() + _ <- log.debug( + "Creating project [{}, {}, {}].", + projectName, + projectId, + projectTemplate + ) + name <- getNameForNewProject(projectName, projectTemplate) _ <- validateName(name) _ <- checkIfNameExists(name) creationTime <- clock.nowInUtc() @@ -111,6 +117,7 @@ class ProjectService[ path, name, engineVersion, + projectTemplate, missingComponentAction ) _ <- log.debug( @@ -481,4 +488,35 @@ class ProjectService[ ) } + private def getNameForNewProject( + projectName: String, + projectTemplate: Option[String] + ): F[ProjectServiceFailure, String] = { + def mkName(name: String, suffix: Int): String = + s"${name}_${suffix}" + def findAvailableName( + projectName: String, + suffix: Int + ): F[ProjectRepositoryFailure, String] = { + val newName = mkName(projectName, suffix) + CovariantFlatMap[F].ifM(repo.exists(newName))( + ifTrue = findAvailableName(projectName, suffix + 1), + ifFalse = CovariantFlatMap[F].pure(newName) + ) + } + + projectTemplate match { + case Some(_) => + CovariantFlatMap[F] + .ifM(repo.exists(projectName))( + ifTrue = findAvailableName(projectName, 1), + ifFalse = CovariantFlatMap[F].pure(projectName) + ) + .mapError(toServiceFailure) + case None => + CovariantFlatMap[F].pure(projectName) + } + + } + } diff --git a/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/service/ProjectServiceApi.scala b/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/service/ProjectServiceApi.scala index f79f0286ab6..e36d87e6a79 100644 --- a/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/service/ProjectServiceApi.scala +++ b/lib/scala/project-manager/src/main/scala/org/enso/projectmanager/service/ProjectServiceApi.scala @@ -19,8 +19,9 @@ trait ProjectServiceApi[F[+_, +_]] { /** Creates a user project. * * @param progressTracker the actor to send progress updates to - * @param name the name of th project + * @param name the name of the project * @param engineVersion Enso version to use for the new project + * @param projectTemplate the name of the project template * @param missingComponentAction specifies how to handle missing components * @return projectId */ @@ -28,6 +29,7 @@ trait ProjectServiceApi[F[+_, +_]] { progressTracker: ActorRef, name: String, engineVersion: SemVer, + projectTemplate: Option[String], missingComponentAction: MissingComponentAction ): F[ProjectServiceFailure, UUID] diff --git a/lib/scala/project-manager/src/test/scala/org/enso/projectmanager/ProjectManagementOps.scala b/lib/scala/project-manager/src/test/scala/org/enso/projectmanager/ProjectManagementOps.scala index 1989c0e1d16..8ce75ca54a9 100644 --- a/lib/scala/project-manager/src/test/scala/org/enso/projectmanager/ProjectManagementOps.scala +++ b/lib/scala/project-manager/src/test/scala/org/enso/projectmanager/ProjectManagementOps.scala @@ -17,12 +17,17 @@ trait ProjectManagementOps { this: BaseServerSpec => def createProject( name: String, + projectTemplate: Option[String] = None, missingComponentAction: Option[MissingComponentAction] = None )(implicit client: WsTestClient): UUID = { val fields = Seq("name" -> name.asJson) ++ missingComponentAction .map(a => "missingComponentAction" -> a.asJson) + .toSeq ++ + projectTemplate + .map(t => "projectTemplate" -> t.asJson) .toSeq + val params = Json.obj(fields: _*) val request = json""" { "jsonrpc": "2.0", diff --git a/lib/scala/project-manager/src/test/scala/org/enso/projectmanager/protocol/ProjectManagementApiSpec.scala b/lib/scala/project-manager/src/test/scala/org/enso/projectmanager/protocol/ProjectManagementApiSpec.scala index 46acac54608..0545cd2ac75 100644 --- a/lib/scala/project-manager/src/test/scala/org/enso/projectmanager/protocol/ProjectManagementApiSpec.scala +++ b/lib/scala/project-manager/src/test/scala/org/enso/projectmanager/protocol/ProjectManagementApiSpec.scala @@ -6,10 +6,10 @@ import org.apache.commons.io.FileUtils import org.enso.editions.SemVerJson._ import org.enso.projectmanager.{BaseServerSpec, ProjectManagementOps} import org.enso.testkit.{FlakySpec, RetrySpec} - import java.io.File -import java.nio.file.Paths +import java.nio.file.{Files, Paths} import java.util.UUID + import scala.io.Source class ProjectManagementApiSpec @@ -171,6 +171,56 @@ class ProjectManagementApiSpec meta shouldBe Symbol("file") } + "create project from default template" in { + val projectName = "Foo" + + implicit val client = new WsTestClient(address) + + createProject(projectName, projectTemplate = Some("default")) + + val projectDir = new File(userProjectDir, projectName) + val packageFile = new File(projectDir, "package.yaml") + val mainEnso = Paths.get(projectDir.toString, "src", "Main.enso").toFile + val meta = Paths.get(projectDir.toString, ".enso", "project.json").toFile + + packageFile shouldBe Symbol("file") + mainEnso shouldBe Symbol("file") + meta shouldBe Symbol("file") + } + + "create project from example template" in { + val projectName = "Foo" + + implicit val client = new WsTestClient(address) + + createProject(projectName, projectTemplate = Some("example")) + + val projectDir = new File(userProjectDir, projectName) + val packageFile = new File(projectDir, "package.yaml") + val mainEnso = Paths.get(projectDir.toString, "src", "Main.enso").toFile + val helloTxt = Paths.get(projectDir.toString, "hello.txt").toFile + val meta = Paths.get(projectDir.toString, ".enso", "project.json").toFile + + packageFile shouldBe Symbol("file") + mainEnso shouldBe Symbol("file") + helloTxt shouldBe Symbol("file") + meta shouldBe Symbol("file") + } + + "find a name when project is created from template" in { + val projectName = "Foo" + + implicit val client = new WsTestClient(address) + + createProject(projectName, projectTemplate = Some("default")) + createProject(projectName, projectTemplate = Some("default")) + + val projectDir = new File(userProjectDir, "Foo_1") + val packageFile = new File(projectDir, "package.yaml") + + Files.readAllLines(packageFile.toPath) contains "name: Foo_1" + } + "create project with specific version" in { implicit val client = new WsTestClient(address) client.send(json""" diff --git a/lib/scala/project-manager/src/test/scala/org/enso/projectmanager/protocol/ProjectOpenSpecBase.scala b/lib/scala/project-manager/src/test/scala/org/enso/projectmanager/protocol/ProjectOpenSpecBase.scala index 08ef588dab0..bd1d45d6f98 100644 --- a/lib/scala/project-manager/src/test/scala/org/enso/projectmanager/protocol/ProjectOpenSpecBase.scala +++ b/lib/scala/project-manager/src/test/scala/org/enso/projectmanager/protocol/ProjectOpenSpecBase.scala @@ -27,18 +27,20 @@ abstract class ProjectOpenSpecBase val blackhole = system.actorOf(blackholeProps) val ordinaryAction = projectService.createUserProject( - blackhole, - "Proj_1", - defaultVersion, - MissingComponentAction.Fail + progressTracker = blackhole, + projectName = "Proj_1", + projectTemplate = None, + engineVersion = defaultVersion, + missingComponentAction = MissingComponentAction.Fail ) ordinaryProject = Runtime.default.unsafeRun(ordinaryAction) val brokenName = "Projbroken" val brokenAction = projectService.createUserProject( - blackhole, - brokenName, - defaultVersion, - MissingComponentAction.Fail + progressTracker = blackhole, + projectName = brokenName, + projectTemplate = None, + engineVersion = defaultVersion, + missingComponentAction = MissingComponentAction.Fail ) brokenProject = Runtime.default.unsafeRun(brokenAction) diff --git a/lib/scala/runtime-version-manager/src/main/scala/org/enso/runtimeversionmanager/runner/Runner.scala b/lib/scala/runtime-version-manager/src/main/scala/org/enso/runtimeversionmanager/runner/Runner.scala index 569bcafde56..3b873a474af 100644 --- a/lib/scala/runtime-version-manager/src/main/scala/org/enso/runtimeversionmanager/runner/Runner.scala +++ b/lib/scala/runtime-version-manager/src/main/scala/org/enso/runtimeversionmanager/runner/Runner.scala @@ -44,6 +44,7 @@ class Runner( path: Path, name: String, engineVersion: SemVer, + projectTemplate: Option[String], authorName: Option[String], authorEmail: Option[String], additionalArguments: Seq[String] @@ -53,13 +54,15 @@ class Runner( authorName.map(Seq("--new-project-author-name", _)).getOrElse(Seq()) val authorEmailOption = authorEmail.map(Seq("--new-project-author-email", _)).getOrElse(Seq()) + val templateOption = + projectTemplate.map(Seq("--new-project-template", _)).getOrElse(Seq()) val arguments = Seq( "--new", path.toAbsolutePath.normalize.toString, "--new-project-name", name - ) ++ authorNameOption ++ authorEmailOption ++ additionalArguments + ) ++ templateOption ++ authorNameOption ++ authorEmailOption ++ additionalArguments // TODO [RW] reporting warnings to the IDE (#1710) if (Engine.isNightly(engineVersion)) { Logger[Runner].warn(