Skip to content

Commit

Permalink
feat: GooeyCLI - replacement utility scripting system based on PicoCLI.
Browse files Browse the repository at this point in the history
Still a proof of concept, with a limited amount of old functionality converted to showcase the new approach. Relies on a groovyw tweak, renames the scripts, and lets PicoCLI worry about all the CLI structuring
  • Loading branch information
Cervator committed Jul 11, 2020
1 parent a250806 commit 05ca0f8
Show file tree
Hide file tree
Showing 20 changed files with 423 additions and 1,188 deletions.
17 changes: 17 additions & 0 deletions config/groovy/BaseCommand.groovy
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Copyright 2020 The Terasology Foundation
// SPDX-License-Identifier: Apache-2.0

import picocli.CommandLine.Command

/**
* Simple super class for commands with global style options
*/
@Command(
synopsisHeading = "%n@|green Usage:|@%n%n",
descriptionHeading = "%n@|green Description:|@%n%n",
parameterListHeading = "%n@|green Parameters:|@%n%n",
optionListHeading = "%n@|green Options:|@%n%n",
commandListHeading = "%n@|green Commands:|@%n%n")
class BaseCommand {

}
37 changes: 37 additions & 0 deletions config/groovy/Get.groovy
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// Copyright 2020 The Terasology Foundation
// SPDX-License-Identifier: Apache-2.0

import picocli.CommandLine.ParentCommand
import picocli.CommandLine.Command
import picocli.CommandLine.Mixin // Actually in use, annotation below may show syntax error due to Groovy's annotation by the same name. Works fine
import picocli.CommandLine.Parameters

/**
* Sub-sub-command that works on item-oriented sub-commands.
* Mixes-in GitOptions to vary the origin if indicated by the user
* Distinct from the sibling command add-remote which does *not* mix in GitOptions since the command itself involves git remote
*/
@Command(name = "get", description = "Gets one or more items directly")
class Get extends BaseCommand implements Runnable {

/** Reference to the parent item command so we can figure out what type it is */
@ParentCommand
ItemCommand parent

/** Mix in a variety of supported Git extras */
@Mixin
GitOptions gitOptions

@Parameters(paramLabel = "items", arity = "1", description = "Target item(s) to get")
List<String> items

void run() {
println "Going to get $items! And from origin: " + gitOptions.origin

// The parent should be a ManagedItem. Make an instace including the possible git origin option
ManagedItem mi = parent.getManager(gitOptions.origin)

// Having prepared an instance of the logic class we call it to actually retrieve stuff
mi.retrieve(items, false)
}
}
12 changes: 12 additions & 0 deletions config/groovy/GitOptions.groovy
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// Copyright 2020 The Terasology Foundation
// SPDX-License-Identifier: Apache-2.0

import picocli.CommandLine.Option

/**
* A mix-in meant to supply some general Git-related options
*/
class GitOptions {
@Option(names = [ "-o", "--origin"], description = "Which Git origin (account) to target")
String origin
}
36 changes: 36 additions & 0 deletions config/groovy/GooeyCLI.groovy
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// Grab the Groovy extensions for PicoCLI - in IntelliJ Alt-ENTER on a `@Grab` to register contents for syntax highlighting
@Grab('info.picocli:picocli-groovy:4.3.2')
// TODO: Actually exists inside the Gradle Wrapper - gradle-6.4.1\lib\groovy-all-1.3-2.5.10.jar\groovyjarjarpicocli\

// TODO: Unsure if this helps or should be included - don't really need this since we execute via Groovy Wrapper anyway
@GrabExclude('org.codehaus.groovy:groovy-all')

// Needed for colors to work on Windows, along with a mode toggle at the start and end of execution in main
@Grab('org.fusesource.jansi:jansi:1.18') // TODO: Exists at 1.17 inside the Gradle Wrapper lib - can use that one?
import org.fusesource.jansi.AnsiConsole

import picocli.CommandLine
import picocli.CommandLine.Command
import picocli.CommandLine.HelpCommand

// If using local groovy files the subcommands section may highlight as bad syntax in IntelliJ - that's OK
@Command(name = "gw",
synopsisSubcommandLabel = "COMMAND", // Default is [COMMAND] indicating optional, but sub command here is required
subcommands = [
HelpCommand.class, // Adds standard help options (help as a subcommand, -h, and --help)
Module.class,
Init.class], // Note that these Groovy classes *must* start with a capital letter for some reason
description = "Utility system for interacting with a Terasology developer workspace")
class GooeyCLI extends BaseCommand {
static void main(String[] args) {
AnsiConsole.systemInstall() // enable colors on Windows - TODO: Test on not-so-Windows systems, should those not run this?
CommandLine cmd = new CommandLine(new GooeyCLI())
if (args.length == 0) {
cmd.usage(System.out)
}
else {
cmd.execute(args)
}
AnsiConsole.systemUninstall() // cleanup when done
}
}
27 changes: 27 additions & 0 deletions config/groovy/Init.groovy
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// Copyright 2020 The Terasology Foundation
// SPDX-License-Identifier: Apache-2.0

import picocli.CommandLine.Command
import picocli.CommandLine.Help.Ansi
import picocli.CommandLine.Mixin // Is in use, IDE may think the Groovy-supplied is in use below and mark this unused
import picocli.CommandLine.Parameters

@Command(name = "init", description = "Initializes a workspace with some useful things")
class Init extends BaseCommand implements Runnable {

/** The name of the distro, if given. Optional parameter (the arity = 0..1 bit) */
@Parameters(paramLabel = "distro", arity = "0..1", defaultValue = "sample", description = "Target module distro to prepare locally")
String distro

/** Mix in a variety of supported Git extras */
@Mixin
GitOptions gitOptions

void run() {
String str = Ansi.AUTO.string("@|bold,green,underline Time to initialize $distro !|@")
System.out.println(str)
println "Do we have a Git origin override? " + gitOptions.origin
println "Can has desired global prop? " + PropHelper.getGlobalProp("alternativeGithubHome")
// Call logic elsewhere
}
}
14 changes: 14 additions & 0 deletions config/groovy/ItemCommand.groovy
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Copyright 2020 The Terasology Foundation
// SPDX-License-Identifier: Apache-2.0

/**
* Simple command type class that indicates the command deals with "items" - nested Git roots representing application elements
*/
abstract class ItemCommand extends BaseCommand {
/**
* Return a manager class for interacting with the specific type of item
* @param optionGitOrigin if the user indicated an alternative Git origin it will be used to vary some URLs
* @return the instantiated item type manager class
*/
abstract ManagedItem getManager(String optionGitOrigin)
}
143 changes: 143 additions & 0 deletions config/groovy/ManagedItem.groovy
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
// Copyright 2020 The Terasology Foundation
// SPDX-License-Identifier: Apache-2.0

@GrabResolver(name = 'jcenter', root = 'http://jcenter.bintray.com/')
@Grab(group = 'org.ajoberstar', module = 'grgit', version = '1.9.3')
import org.ajoberstar.grgit.Grgit

/**
* Utility class for dealing with items managed in a developer workspace.
*
* Primarily assists with the retrieval, creation, and updating of nested Git roots representing application elements.
*/
abstract class ManagedItem {

/** For keeping a list of items retrieved so far (retrieval calls may be recursive) */
def itemsRetrieved = []

/** The default name of a git remote we might want to work on or keep handy */
String defaultRemote = "origin" // TODO: Consider always naming remotes after their origin / account name?

String displayName
abstract String getDisplayName()

File targetDirectory
abstract File getTargetDirectory()

String githubTargetHome
abstract String getDefaultItemGitOrigin()

ManagedItem() {
displayName = getDisplayName()
targetDirectory = getTargetDirectory()
githubTargetHome = calculateGitOrigin(null)
}

ManagedItem(String optionGitOrigin) {
displayName = getDisplayName()
targetDirectory = getTargetDirectory()
githubTargetHome = calculateGitOrigin(optionGitOrigin)
}

String calculateGitOrigin(String optionOrigin) {
// If the user indicated a target Git origin via option parameter then use that (primary choice)
if (optionOrigin != null) {
println "We have an option set for Git origin so using that: " + optionOrigin
return optionOrigin
}

// Alternatively if the user has a global override set for Git origin then use that (secondary choice)
String altOrigin = PropHelper.getGlobalProp("alternativeGithubHome")
if (altOrigin != null) {
println "There was no option set but we have a global proper override for Git origin: " + altOrigin
return altOrigin
}

// And finally if neither override is set fall back on the default defined by the item type
println "No option nor global override set for Git origin so using default for the type: " + getDefaultItemGitOrigin()
return getDefaultItemGitOrigin()
}

// TODO: Likely everything below should just delegate to more specific classes to keep things tidy
// TODO: That would allow these methods to later just figure out exact required operations then delegate
// TODO: Should make it easier to hide the logic of (for instance) different Git adapters behind the next step

/**
* Tests a URL via a HEAD request (no body) to see if it is valid
* @param url the URL to test
* @return boolean indicating whether the URL is valid (code 200) or not
*/
boolean isUrlValid(String url) {
def code = new URL(url).openConnection().with {
requestMethod = 'HEAD'
connect()
responseCode
}
return code.toString() == "200"
}

/**
* Primary entry point for retrieving items, kicks off recursively if needed.
* @param items the items we want to retrieve
* @param recurse whether to also retrieve dependencies of the desired items (only really for modules ...)
*/
def retrieve(List<String> items, boolean recurse) {
println "Now inside retrieve, user (recursively? $recurse) wants: $items"
for (String itemName : items) {
println "Starting retrieval for $displayName $itemName, are we recursing? $recurse"
println "Retrieved so far: $itemsRetrieved"
retrieveItem(itemName, recurse)
}
}

/**
* Retrieves a single item via Git Clone. Considers whether it exists locally first or if it has already been retrieved this execution.
* @param itemName the target item to retrieve
* @param recurse whether to also retrieve its dependencies (if so then recurse back into retrieve)
*/
def retrieveItem(String itemName, boolean recurse) {
File itemDir = new File(targetDirectory, itemName)
println "Request to retrieve $displayName $itemName would store it at $itemDir - exists? " + itemDir.exists()
if (itemDir.exists()) {
println "That $displayName already had an existing directory locally. If something is wrong with it please delete and try again"
itemsRetrieved << itemName
} else if (itemsRetrieved.contains(itemName)) {
println "We already retrieved $itemName - skipping"
} else {
itemsRetrieved << itemName
def targetUrl = "https://github.com/${githubTargetHome}/${itemName}"
if (!isUrlValid(targetUrl)) {
println "Can't retrieve $displayName from $targetUrl - URL appears invalid. Typo? Not created yet?"
return
}
println "Retrieving $displayName $itemName from $targetUrl"
if (githubTargetHome != getDefaultItemGitOrigin()) {
println "Doing a retrieve from a custom remote: $githubTargetHome - will name it as such plus add the ${getDefaultItemGitOrigin()} remote as '$defaultRemote'"
Grgit.clone dir: itemDir, uri: targetUrl, remote: githubTargetHome
println "Primary clone operation complete, about to add the '$defaultRemote' remote for the ${getDefaultItemGitOrigin()} org address"
//addRemote(itemName, defaultRemote, "https://github.com/${getDefaultItemGitOrigin()}/${itemName}") //TODO: Add me :p
} else {
println "Cloning $targetUrl to $itemDir"
Grgit.clone dir: itemDir, uri: targetUrl
}
/*
// This step allows the item type to check the newly cloned item and add in extra template stuff - TODO?
//itemTypeScript.copyInTemplateFiles(itemDir)
// Handle also retrieving dependencies if the item type cares about that
if (recurse) {
def foundDependencies = itemTypeScript.findDependencies(itemDir)
if (foundDependencies.length == 0) {
println "The $itemType $itemName did not appear to have any dependencies we need to worry about"
} else {
println "The $itemType $itemName has the following $itemType dependencies we care about: $foundDependencies"
String[] uniqueDependencies = foundDependencies - itemsRetrieved
println "After removing dupes already retrieved we have the remaining dependencies left: $uniqueDependencies"
if (uniqueDependencies.length > 0) {
retrieve(uniqueDependencies, true)
}
}
}*/
}
}
}
46 changes: 46 additions & 0 deletions config/groovy/ManagedModule.groovy
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// Copyright 2020 The Terasology Foundation
// SPDX-License-Identifier: Apache-2.0

class ManagedModule extends ManagedItem {
ManagedModule() {
super()
}

ManagedModule(String optionGitOrigin) {
super(optionGitOrigin)
}

@Override
String getDisplayName() {
return "module"
}

@Override
File getTargetDirectory() {
return new File("modules")
}

@Override
String getDefaultItemGitOrigin() {
return "Terasology"
}

/**
* Copies in a fresh copy of build.gradle for all modules (in case changes are made and need to be propagated)
*/
void refreshGradle() {
targetDirectory.eachDir() { dir ->
File targetDir = new File(targetDirectory, dir.name)

// Copy in the template build.gradle for modules
if (!new File(targetDir, "module.txt").exists()) {
println "$targetDir has no module.txt, it must not want a fresh build.gradle"
return
}
println "In refreshGradle for module $targetDir - copying in a fresh build.gradle"
File targetBuildGradle = new File(targetDir, 'build.gradle')
targetBuildGradle.delete()
targetBuildGradle << new File('templates/build.gradle').text
}
}
}
30 changes: 30 additions & 0 deletions config/groovy/PropHelper.groovy
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// Copyright 2020 The Terasology Foundation
// SPDX-License-Identifier: Apache-2.0

/**
* Convenience class for pulling properties out of a 'gradle.properties' if present, for various overrides
* // TODO: YAML variant that can be added for more complex use cases?
*/
class PropHelper {

static String getGlobalProp(String key) {
Properties extraProps = new Properties()
File gradlePropsFile = new File("gradle.properties")
if (gradlePropsFile.exists()) {
gradlePropsFile.withInputStream {
extraProps.load(it)
}
//println "Found a 'gradle.properties' file, loaded in global overrides: " + extraProps

if (extraProps.containsKey(key)) {
println "Returning found global prop for $key"
return extraProps.get(key)
}
println "Didn't find a global prop for key $key"

} else {
println "No 'gradle.properties' file found, not supplying global overrides"
}
return null
}
}

0 comments on commit 05ca0f8

Please sign in to comment.