org.grails.cli.profile.commands.CreateAppCommand.groovy Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of grace-shell Show documentation
Show all versions of grace-shell Show documentation
Grace Framework : Grace Shell
/*
* Copyright 2014-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* 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.
*/
package org.grails.cli.profile.commands
import java.nio.file.FileVisitResult
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.Paths
import java.nio.file.SimpleFileVisitor
import java.nio.file.attribute.BasicFileAttributes
import java.util.stream.Stream
import groovy.ant.AntBuilder
import groovy.text.GStringTemplateEngine
import groovy.text.Template
import groovy.text.TemplateEngine
import groovy.transform.CompileDynamic
import groovy.transform.CompileStatic
import groovy.transform.TypeCheckingMode
import org.apache.tools.ant.BuildLogger
import org.apache.tools.ant.MagicNames
import org.apache.tools.ant.Project
import org.apache.tools.ant.ProjectHelper
import org.apache.tools.ant.types.ResourceCollection
import org.apache.tools.ant.types.resources.FileResource
import org.apache.tools.ant.types.resources.URLResource
import org.eclipse.aether.graph.Dependency
import org.yaml.snakeyaml.LoaderOptions
import org.yaml.snakeyaml.Yaml
import org.yaml.snakeyaml.constructor.SafeConstructor
import grails.build.logging.GrailsConsole
import grails.io.IOUtils
import grails.util.GrailsNameUtils
import grails.util.GrailsVersion
import org.grails.build.logging.GrailsConsoleAntBuilder
import org.grails.build.logging.GrailsConsoleAntProject
import org.grails.build.logging.GrailsConsoleLogger
import org.grails.build.parsing.CommandLine
import org.grails.cli.GrailsCli
import org.grails.cli.profile.CommandDescription
import org.grails.cli.profile.ExecutionContext
import org.grails.cli.profile.Feature
import org.grails.cli.profile.Profile
import org.grails.cli.profile.ProfileRepository
import org.grails.cli.profile.ProfileRepositoryAware
import org.grails.cli.profile.commands.io.GradleDependency
import org.grails.cli.profile.repository.MavenProfileRepository
import org.grails.config.NavigableMap
import org.grails.io.support.FileSystemResource
import org.grails.io.support.Resource
import static org.grails.build.parsing.CommandLine.QUIET_ARGUMENT
import static org.grails.build.parsing.CommandLine.STACKTRACE_ARGUMENT
import static org.grails.build.parsing.CommandLine.VERBOSE_ARGUMENT
/**
* Command for creating Grails applications
*
* @author Graeme Rocher
* @author Lari Hotari
* @author Michael Yan
* @since 3.0
*/
@CompileStatic
class CreateAppCommand extends ArgumentCompletingCommand implements ProfileRepositoryAware {
public static final String NAME = 'create-app'
public static final String PROFILE_FLAG = 'profile'
public static final String FEATURES_FLAG = 'features'
public static final String TEMPLATE_FLAG = 'template'
public static final String CSS_FLAG = 'css'
public static final String JAVASCRIPT_FLAG = 'javascript'
public static final String DATABASE_FLAG = 'database'
public static final String ENABLE_PREVIEW_FLAG = 'enable-preview'
public static final String ENCODING = System.getProperty('file.encoding') ?: 'UTF-8'
public static final String INPLACE_FLAG = 'inplace'
public static final String FORCE_FLAG = 'force'
public static final String GRACE_VERSION_FLAG = 'grace-version'
public static final String[] SUPPORT_GRACE_VERSIONS = ['2023', '2022', '6', '5', '4', '3']
public static final String UNZIP_PROFILE_TEMP_DIR = 'grails-profile-'
public static final String UNZIP_TEMPLATE_TEMP_DIR = 'grails-template-'
protected static final String APPLICATION_YML = 'application.yml'
protected static final String BUILD_GRADLE = 'build.gradle'
protected static final String GRADLE_PROPERTIES = 'gradle.properties'
ProfileRepository profileRepository
CommandDescription description = new CommandDescription(name, 'Creates an application', 'create-app [NAME] --profile=web')
CreateAppCommand() {
populateDescription()
description.flag(name: INPLACE_FLAG, description: 'Used to create an application using the current directory')
description.flag(name: PROFILE_FLAG, description: 'The profile to use', required: false)
description.flag(name: FEATURES_FLAG, description: 'The features to use', required: false)
description.flag(name: TEMPLATE_FLAG, description: 'The application template to use', required: false)
description.flag(name: CSS_FLAG, description: 'The CSS framework to use', required: false)
description.flag(name: JAVASCRIPT_FLAG, description: 'The JavaScript approach', required: false)
description.flag(name: DATABASE_FLAG, description: 'The Database type', required: false)
description.flag(name: STACKTRACE_ARGUMENT, description: 'Show full stacktrace', required: false)
description.flag(name: VERBOSE_ARGUMENT, description: 'Show verbose output', required: false)
description.flag(name: QUIET_ARGUMENT, description: 'Suppress status output', required: false)
description.flag(name: ENABLE_PREVIEW_FLAG, description: 'Enable preview features', required: false)
description.flag(name: GRACE_VERSION_FLAG, description: 'Specific Grace Version', required: false)
description.flag(name: FORCE_FLAG, description: 'Force overwrite of existing files', required: false)
}
protected void populateDescription() {
description.argument(name: 'Application Name', description: 'The name of the application to create.', required: false)
}
@Override
String getName() {
NAME
}
protected String getDefaultProfile() {
ProfileRepository.DEFAULT_PROFILE_NAME
}
@Override
protected int complete(CommandLine commandLine, CommandDescription desc, List candidates, int cursor) {
Map.Entry lastOption = commandLine.lastOption()
if (lastOption != null) {
// if value == true it means no profile is specified and only the flag is present
List profileNames = this.profileRepository.allProfiles*.name
if (lastOption.key == PROFILE_FLAG) {
def val = lastOption.value
if (val == true) {
candidates.addAll(profileNames)
return cursor
}
else if (!profileNames.contains(val)) {
String valStr = val.toString()
List candidateProfiles = profileNames.findAll { String pn ->
pn.startsWith(valStr)
}.collect { String pn ->
"${pn.substring(valStr.size())} ".toString()
}
candidates.addAll candidateProfiles
return cursor
}
}
else if (lastOption.key == FEATURES_FLAG) {
Object val = lastOption.value
Profile profile = this.profileRepository.getProfile(commandLine.hasOption(PROFILE_FLAG) ?
commandLine.optionValue(PROFILE_FLAG).toString() : getDefaultProfile())
List featureNames = profile.features*.name
if (val == true) {
candidates.addAll(featureNames)
return cursor
}
else if (!profileNames.contains(val)) {
String valStr = val.toString()
if (valStr.endsWith(',')) {
String[] specified = valStr.split(',')
candidates.addAll(featureNames.findAll { String f ->
!specified.contains(f)
})
return cursor
}
List candidatesFeatures = featureNames.findAll { String pn ->
pn.startsWith(valStr)
}.collect { String pn ->
"${pn.substring(valStr.size())} ".toString()
}
candidates.addAll candidatesFeatures
return cursor
}
}
}
super.complete(commandLine, desc, candidates, cursor)
}
@Override
boolean handle(ExecutionContext executionContext) {
GrailsConsole console = executionContext.console
CommandLine commandLine = executionContext.commandLine
String profileName = commandLine.optionValue('profile')?.toString() ?: getDefaultProfile()
List validFlags = [INPLACE_FLAG, PROFILE_FLAG, FEATURES_FLAG, TEMPLATE_FLAG,
CSS_FLAG, JAVASCRIPT_FLAG, DATABASE_FLAG,
STACKTRACE_ARGUMENT, VERBOSE_ARGUMENT, QUIET_ARGUMENT, GRACE_VERSION_FLAG, FORCE_FLAG]
if (!commandLine.hasOption(ENABLE_PREVIEW_FLAG)) {
commandLine.undeclaredOptions.each { String key, Object value ->
if (!validFlags.contains(key)) {
List possibleSolutions = validFlags.findAll { it.substring(0, 2) == key.substring(0, 2) }
StringBuilder warning = new StringBuilder("Unrecognized flag: ${key}.")
if (possibleSolutions) {
warning.append(' Possible solutions: ')
warning.append(possibleSolutions.join(', '))
}
console.warn(warning.toString())
}
}
}
String grailsVersion = GrailsVersion.current().version
String specificGraceVersion = commandLine.optionValue(GRACE_VERSION_FLAG)
boolean inPlace = commandLine.hasOption('inplace') || GrailsCli.isInteractiveModeActive()
String appName = commandLine.remainingArgs ? commandLine.remainingArgs[0] : ''
List features = commandLine.optionValue('features')?.toString()?.split(',')?.toList()
Map args = getCommandArguments(commandLine)
CreateAppCommandObject cmd = new CreateAppCommandObject(
appName: appName,
baseDir: executionContext.baseDir,
profileName: profileName,
grailsVersion: specificGraceVersion ?: grailsVersion,
features: features,
template: commandLine.optionValue('template'),
inplace: inPlace,
stacktrace: commandLine.hasOption(STACKTRACE_ARGUMENT),
verbose: commandLine.hasOption(VERBOSE_ARGUMENT),
quiet: commandLine.hasOption(QUIET_ARGUMENT),
force: commandLine.hasOption(FORCE_FLAG),
console: console,
args: args
)
if (commandLine.hasOption(GRACE_VERSION_FLAG) && specificGraceVersion != grailsVersion) {
if (validateSpecificGraceVersion(specificGraceVersion)) {
this.profileRepository = new MavenProfileRepository(specificGraceVersion)
}
else {
console.error("Specific Grace version `$specificGraceVersion` is not supported!")
return false
}
}
this.handle(cmd)
}
boolean handle(CreateAppCommandObject cmd) {
if (this.profileRepository == null) {
throw new IllegalStateException("Property 'profileRepository' must be set")
}
String grailsVersion = cmd.grailsVersion
GrailsConsole console = cmd.console
String profileName = cmd.profileName
Profile profileInstance = this.profileRepository.getProfile(profileName)
if (!validateProfile(profileInstance, profileName, console)) {
return false
}
if (!cmd.appName && !cmd.inplace) {
console.error('Specify an application name or use --inplace to create an application in the current directory')
return false
}
String appName
String groupName
String defaultPackageName
String groupAndAppName = cmd.appName
if (cmd.inplace) {
appName = new File('.').canonicalFile.name
groupAndAppName = groupAndAppName ?: appName
}
if (!groupAndAppName) {
console.error('Specify an application name or use --inplace to create an application in the current directory')
return false
}
try {
List parts = groupAndAppName.split(/\./) as List
if (parts.size() == 1) {
appName = parts[0]
defaultPackageName = appName.split(/[-]+/).collect { String token ->
(token.toLowerCase().toCharArray().findAll { char ch ->
Character.isJavaIdentifierPart(ch)
} as char[]) as String
}.join('.')
if (!GrailsNameUtils.isValidJavaPackage(defaultPackageName)) {
console.error("Cannot create a valid package name for [$appName]. " +
'Please specify a name that is also a valid Java package.')
return false
}
groupName = defaultPackageName
}
else {
appName = parts[-1]
groupName = parts[0..-2].join('.')
defaultPackageName = groupName
}
}
catch (IllegalArgumentException e) {
console.error(e.message)
return false
}
Path appFullDirectory = Paths.get(cmd.baseDir.path, appName)
File projectTargetDirectory = cmd.inplace ? new File('.').canonicalFile : appFullDirectory.toAbsolutePath().normalize().toFile()
if (projectTargetDirectory.exists() && !isDirectoryEmpty(projectTargetDirectory)) {
if (cmd.force) {
cleanDirectory(projectTargetDirectory)
}
else {
console.error("Directory `${projectTargetDirectory.absolutePath}` already exists!" +
' Use --force if you want to overwrite or specify an alternate location.')
return false
}
}
else {
boolean result = projectTargetDirectory.mkdir()
if (!result) {
console.error("Directory `${projectTargetDirectory.absolutePath}` created faild!")
}
}
List features = evaluateFeatures(profileInstance, cmd.features, cmd.console)
Map variables = initializeVariables(appName, groupName, defaultPackageName, profileName, features, cmd.template, cmd.grailsVersion)
Map args = new HashMap<>()
args.putAll(cmd.args)
args.put(FEATURES_FLAG, features*.name?.join(','))
Project project = createAntProject(cmd.appName, projectTargetDirectory, variables, args, console, cmd.verbose, cmd.quiet)
GrailsConsoleAntBuilder ant = new GrailsConsoleAntBuilder(project)
String projectType = getName().substring(7)
console.addStatus("Creating a new ${projectType == 'app' ? 'application' : projectType}")
console.println()
console.println(" Name:".padRight(20) + appName)
console.println(" Package:".padRight(20) + defaultPackageName)
console.println(" Profile:".padRight(20) + profileName)
if (features) {
console.println(" Features:".padRight(20) + features*.name?.join(', '))
}
if (cmd.template) {
console.println(" Template:".padRight(20) + cmd.template)
}
console.println(" Project root:".padRight(20) + projectTargetDirectory.absolutePath)
console.println()
generateProjectSkeleton(ant, profileInstance, features, variables, projectTargetDirectory, cmd.verbose, cmd.quiet)
if (cmd.template) {
if (cmd.template.endsWith('.groovy')) {
replaceBuildTokens(ant, profileName, profileInstance, features, variables, grailsVersion, projectTargetDirectory)
applyApplicationTemplate(ant, console, cmd.appName, cmd.template, projectTargetDirectory, cmd.verbose, cmd.quiet)
}
else if (cmd.template.endsWith('.zip') || cmd.template.endsWith('.git') || new File(cmd.template).isDirectory()) {
copyApplicationTemplate(ant, console, appName, groupName, defaultPackageName, profileInstance,
features, cmd.template, grailsVersion, variables, args, projectTargetDirectory, cmd.verbose, cmd.quiet)
}
}
else {
replaceBuildTokens(ant, profileName, profileInstance, features, variables, grailsVersion, projectTargetDirectory)
}
String result = String.format("%s created by %s %s.", projectType == 'app' ? 'Application' : projectType.capitalize(),
GrailsVersion.isGrace(grailsVersion) ? 'Grace' : 'Grails', grailsVersion)
if (cmd.quiet) {
console.updateStatus(result)
}
else {
console.addStatus(result)
}
if (profileInstance.instructions) {
console.addStatus(profileInstance.instructions)
}
GrailsCli.triggerAppLoad()
true
}
protected boolean validateProfile(Profile profileInstance, String profileName, GrailsConsole console) {
if (profileInstance == null) {
console.error("Profile not found for name [$profileName]")
return false
}
true
}
protected List evaluateFeatures(Profile profile, List requestedFeatures, GrailsConsole console) {
List features
if (requestedFeatures) {
List allFeatureNames = profile.features*.name
Collection validFeatureNames = requestedFeatures.intersect(allFeatureNames)
requestedFeatures.removeAll(allFeatureNames)
requestedFeatures.each { String invalidFeature ->
List possibleSolutions = allFeatureNames.findAll {
it.substring(0, 2) == invalidFeature.substring(0, 2)
}
StringBuilder warning = new StringBuilder("Feature ${invalidFeature} does not exist in the profile ${profile.name}!")
if (possibleSolutions) {
warning.append(' Possible solutions: ')
warning.append(possibleSolutions.join(', '))
}
console.warn(warning.toString())
}
features = (profile.features.findAll { Feature f -> validFeatureNames.contains(f.name) } + profile.requiredFeatures).unique()
}
else {
features = (profile.defaultFeatures + profile.requiredFeatures).toList().unique()
}
features?.sort {
it.name
}
}
protected void generateProjectSkeleton(GrailsConsoleAntBuilder ant, Profile profileInstance,
List features, Map variables,
File projectTargetDirectory, boolean verbose, boolean quiet = false) {
List profiles = this.profileRepository.getProfileAndDependencies(profileInstance)
final Map unzippedDirectories = new LinkedHashMap()
Map targetDirs = [:]
buildTargetFolders(profileInstance, targetDirs, projectTargetDirectory)
for (Profile p : profiles) {
Set ymlFiles = findAllFilesByName(projectTargetDirectory, APPLICATION_YML)
Map ymlCache = [:]
File targetDirectory = targetDirs[p]
ymlFiles.each { File applicationYmlFile ->
String previousApplicationYml = (applicationYmlFile.isFile()) ? applicationYmlFile.getText(ENCODING) : null
if (previousApplicationYml) {
ymlCache[applicationYmlFile] = previousApplicationYml
}
}
copySkeleton(ant, profileInstance, p, variables, targetDirectory, unzippedDirectories)
ymlCache.each { File applicationYmlFile, String previousApplicationYml ->
if (applicationYmlFile.exists()) {
appendToYmlSubDocument(applicationYmlFile, previousApplicationYml)
}
}
}
for (Feature f in features) {
Resource location = f.location
File skeletonDir
File tmpDir
if (location instanceof FileSystemResource) {
skeletonDir = location.createRelative('skeleton').file
}
else {
tmpDir = unzipProfile(ant, unzippedDirectories, location)
skeletonDir = new File(tmpDir, "META-INF/grails-profile/features/$f.name/skeleton")
}
File targetDirectory = targetDirs[f.profile]
appendFeatureFiles(skeletonDir, targetDirectory)
if (skeletonDir.exists()) {
copySrcToTarget(ant, skeletonDir, ['**/' + APPLICATION_YML], profileInstance.binaryExtensions, variables, targetDirectory)
}
}
// Cleanup temporal directories
unzippedDirectories.values().each { File tmpDir ->
deleteDirectoryOrFile(tmpDir)
}
}
@CompileStatic(TypeCheckingMode.SKIP)
protected void copySkeleton(GrailsConsoleAntBuilder ant, Profile profile, Profile participatingProfile,
Map variables, File targetDirectory, Map unzippedDirectories) {
List buildMergeProfileNames = profile.buildMergeProfileNames
List excludes = profile.skeletonExcludes
if (profile == participatingProfile) {
excludes = []
}
Resource skeletonResource = participatingProfile.profileDir.createRelative('skeleton')
File skeletonDir
File tmpDir
if (skeletonResource instanceof FileSystemResource) {
skeletonDir = skeletonResource.file
}
else {
// establish the JAR file name and extract
tmpDir = unzipProfile(ant, unzippedDirectories, skeletonResource)
skeletonDir = new File(tmpDir, 'META-INF/grails-profile/skeleton')
}
copySrcToTarget(ant, skeletonDir, excludes, profile.binaryExtensions, variables, targetDirectory)
Set sourceBuildGradles = findAllFilesByName(skeletonDir, BUILD_GRADLE)
sourceBuildGradles.each { File srcFile ->
File srcDir = srcFile.parentFile
File destDir = getDestinationDirectory(srcFile, targetDirectory)
File destFile = new File(destDir, BUILD_GRADLE)
if (new File(srcDir, '.gitattributes').exists()) {
ant.copy(file: "${srcDir}/.gitattributes", todir: destDir, failonerror: false)
}
if (new File(srcDir, '.gitignore').exists()) {
ant.copy(file: "${srcDir}/.gitignore", todir: destDir, failonerror: false)
}
if (!destFile.exists()) {
ant.copy file: srcFile, tofile: destFile
}
else if (buildMergeProfileNames.contains(participatingProfile.name)) {
def concatFile = "${destDir}/concat-build.gradle"
ant.move(file: destFile, tofile: concatFile)
ant.concat([destfile: destFile, fixlastline: true], {
path {
pathelement location: concatFile
pathelement location: srcFile
}
})
ant.delete(file: concatFile, failonerror: false)
}
}
Set sourceGradleProperties = findAllFilesByName(skeletonDir, GRADLE_PROPERTIES)
sourceGradleProperties.each { File srcFile ->
File destDir = getDestinationDirectory(srcFile, targetDirectory)
File destFile = new File(destDir, GRADLE_PROPERTIES)
if (destFile.exists()) {
def concatGradlePropertiesFile = "${destDir}/concat-gradle.properties"
ant.move(file: destFile, tofile: concatGradlePropertiesFile)
ant.concat([destfile: destFile, fixlastline: true], {
path {
pathelement location: concatGradlePropertiesFile
pathelement location: srcFile
}
})
ant.delete(file: concatGradlePropertiesFile, failonerror: false)
}
else {
ant.copy file: srcFile, tofile: destFile
}
}
ant.chmod(dir: targetDirectory, includes: profile.executablePatterns.join(' '), perm: 'u+x')
}
@CompileDynamic
protected void copySrcToTarget(GrailsConsoleAntBuilder ant, File srcDir, List excludes, Set binaryFileExtensions,
Map variables, File targetDirectory) {
ant.copy(todir: targetDirectory, overwrite: true, encoding: 'UTF-8') {
fileSet(dir: srcDir, casesensitive: false) {
for (exc in excludes) {
exclude name: exc
}
exclude name: '**/' + BUILD_GRADLE
exclude name: '**/' + GRADLE_PROPERTIES
binaryFileExtensions.each { ext ->
exclude(name: "**/*.${ext}")
}
}
filterset {
variables.each { k, v ->
filter(token: k, value: v)
}
}
mapper {
filtermapper {
variables.each { k, v ->
replacestring(from: "@${k}@".toString(), to: v)
}
}
}
}
ant.copy(todir: targetDirectory, overwrite: true) {
fileSet(dir: srcDir, casesensitive: false) {
binaryFileExtensions.each { ext ->
include(name: "**/*.${ext}")
}
for (exc in excludes) {
exclude name: exc
}
exclude name: '**/' + BUILD_GRADLE
}
}
}
@CompileDynamic
protected void copyApplicationTemplate(GrailsConsoleAntBuilder ant, GrailsConsole console,
String appName, String groupName, String packageName, Profile profile,
List features, String templateUrl, String grailsVersion,
Map variables, Map args, File targetDirectory,
boolean verbose, boolean quiet = false) {
if (quiet) {
console.updateStatus('Applying Template')
}
else {
console.addStatus('Applying Template')
console.println()
}
// Define Ant tasks
ant.taskdef(resource: 'org/grails/cli/profile/tasks/antlib.xml')
File tempZipFile = null
File tempDir = null
File projectDir = null
try {
if (templateUrl.endsWith('.zip')) {
if (!quiet) {
console.println(" [unzip] src: " + templateUrl)
}
tempZipFile = Files.createTempFile(UNZIP_TEMPLATE_TEMP_DIR, '.zip').toFile()
ant.get(src: templateUrl, dest: tempZipFile)
tempDir = Files.createTempDirectory(UNZIP_TEMPLATE_TEMP_DIR).toFile()
ant.unzip(src: tempZipFile, dest: tempDir)
if (quiet) {
console.updateStatus('Unzip template: ' + templateUrl)
}
else {
console.println()
}
Files.walkFileTree(tempDir.absoluteFile.toPath(), new SimpleFileVisitor() {
@Override
FileVisitResult visitFile(Path path, BasicFileAttributes attributes)
throws IOException {
if (path.fileName.toString() == 'project.yml') {
projectDir = path.parent.toFile()
FileVisitResult.TERMINATE
} else {
FileVisitResult.CONTINUE
}
}
})
} else if (templateUrl.endsWith('.git')) {
if (!quiet) {
console.println(" [git] clone: " + templateUrl)
}
tempDir = Files.createTempDirectory(UNZIP_TEMPLATE_TEMP_DIR).toFile()
ant.exec(executable: 'git') {
arg value: 'clone'
arg value: templateUrl
arg value: tempDir
}
if (quiet) {
console.updateStatus('Clone repository: ' + templateUrl)
}
else {
console.println()
}
Files.walkFileTree(tempDir.absoluteFile.toPath(), new SimpleFileVisitor() {
@Override
FileVisitResult visitFile(Path path, BasicFileAttributes attributes)
throws IOException {
if (path.fileName.toString() == 'project.yml') {
projectDir = path.parent.toFile()
FileVisitResult.TERMINATE
} else {
FileVisitResult.CONTINUE
}
}
})
}
else {
tempDir = Files.createTempDirectory(UNZIP_TEMPLATE_TEMP_DIR).toFile()
ant.copy(todir: tempDir) {
fileSet(dir: templateUrl)
}
projectDir = tempDir
}
if (projectDir == null || !projectDir.isDirectory() || !projectDir.exists()) {
console.error("`${templateUrl}` is not a valid template!")
console.println()
return
}
File projectYml = new File(projectDir, 'project.yml')
File templateDir = new File(projectDir, 'template')
if (!projectYml.exists() || !templateDir.exists() || !templateDir.isDirectory()) {
console.error("`${templateUrl}` is not a valid template!")
console.println()
return
}
Yaml yaml = new Yaml(new SafeConstructor(new LoaderOptions()))
Map templateConfig = yaml.