diff --git a/.goreleaser.yml b/.goreleaser.yml index 4f274bc..9b6e620 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -50,17 +50,18 @@ snapcrafts: confinement: classic license: Apache-2.0 publish: true - summary: "Gum is a Gradle/Maven/jbang wrapper written in Go" + summary: "Gum is a Gradle/Maven/Ant/Bach/JBang wrapper written in Go" description: | - Gum is a Gradle/Maven/jbang wrapper written in Go, inspired in https://github.com/dougborg/gdub and https://github.com/srs/gw. - Gum automatically detects if the project is Gradle, Maven, Bach, or jbang based and runs the appropriate command. However in the case - that Gum guesses wrong you can force a specific build tool to be used. Similarly as gdub, Gum lets you invoke either Gradle or - Maven from anywhere within the project structure, not just the root directory. + Gum is a Gradle/Maven/Ant/Bach/JBang wrapper written in Go, inspired in https://github.com/dougborg/gdub and https://github.com/srs/gw. + Gum automatically detects if the project is Gradle, Maven, Ant, Bach, or JBang based and runs the appropriate command. However + in the case that Gum guesses wrong you can force a specific build tool to be used. Similarly as gdub, Gum lets you invoke either + Gradle, Maven, or Ant from anywhere within the project structure, not just the root directory. **Usage** Gum supports the following flags + * **-ga** force Ant execution * **-gb** force Bach execution * **-gc** displays current configuration and quits * **-gd** displays debug information @@ -74,8 +75,8 @@ snapcrafts: * **-gv** displays version information Gum will execute the build based on the root build file unless **-gn** is specified, in which case the nearest build file - will be selected. If a specific build file is given (**-b**, **--build-file** for Gradle; **-f**, **--file** for Maven) - then that file will be used instead. + will be selected. If a specific build file is given (**-b**, **--build-file** for Gradle; **-f**, **--file** for Maven, + **-f**, **-file**, **-buildfile** for Ant) then that file will be used instead. brews: - name: gum goarm: 6 @@ -84,6 +85,6 @@ brews: name: gum-homebrew-tap folder: Formula homepage: "https://github.com/kordamp/gm" - description: "Gum is a Gradle/Maven/jbang wrapper written in Go" + description: "Gum is a Gradle/Maven/Ant/Bach/JBang wrapper written in Go" license: Apache-2.0 skip_upload: false diff --git a/README.adoc b/README.adoc index 0532f18..d17b781 100644 --- a/README.adoc +++ b/README.adoc @@ -12,18 +12,18 @@ image:https://img.shields.io/github/downloads/{project-owner}/{project-name}/tot --- -Gum is a link:https://gradle.org[Gradle]/link:https:maven.apache.org[Maven]/link:https://github.com/sormuras/bach/[Bach]/link:https://github.com/jbangdev[JBang] -wrapper written in link:https://golang.org/[Go], inspired in link:https://github.com/dougborg/gdub[https://github.com/dougborg/gdub] and +Gum is a link:https://gradle.org[Gradle]/link:https:maven.apache.org[Maven]/link:https://github.com/sormuras/bach/[Bach]/link:https://github.com/jbangdev[JBang]/link:https://ant.apache.org/[Ant] wrapper written in link:https://golang.org/[Go], inspired in link:https://github.com/dougborg/gdub[https://github.com/dougborg/gdub] and link:https://github.com/srs/gw[https://github.com/srs/gw]. -Gum automatically detects if the project is Gradle, Maven, Bach, or jbang based and runs the appropriate command. However in the case that Gum guesses wrong -you canforce a specific build tool to be used. Similarly as gdub, Gum lets you invoke either Gradle or Maven from anywhere within the project structure, -not just the root directory. +Gum automatically detects if the project is Gradle, Maven, Bach, JBang or Ant based and runs the appropriate command. +However in the case that Gum guesses wrong you canforce a specific build tool to be used. Similarly as gdub, Gum lets +you invoke either Gradle, Maven, or Ant from anywhere within the project structure, not just the root directory. == Usage Gum supports the following flags +* *-ga* force Ant execution * *-gb* force Bach execution * *-gc* displays current configuration and quits * *-gd* displays debug information @@ -36,8 +36,9 @@ Gum supports the following flags * *-gr* do not replace goals/tasks * *-gv* displays version information -Gum will execute the build based on the root build file unless *-gn* is specified, in which case the nearest build file will be selected. -If a specific build file is given (*-b*, *--build-file* for Gradle; *-f*, *--file* for Maven) then that file will be used instead. +Gum will execute the build based on the root build file unless *-gn* is specified, in which case the nearest build file +will be selected. If a specific build file is given (*-b*, *--build-file* for Gradle; *-f*, *--file* for Maven, *-f*, +*-file*, *-buildfile* for Ant) then that file will be used instead. Gum works by passing the given arguments to the resolved tool; it will replace common goal/task names following these mappings @@ -119,7 +120,7 @@ quiet = false debug = false # tool discovery order # default order is the following -discovery = ["gradle", "maven", "bach", "jbang"] +discovery = ["gradle", "maven", "ant", "bach", "jbang"] [gradle] # if goal/tasks should be replaced, same as passing -gr diff --git a/gm.go b/gm.go index 2a24338..2237f0f 100644 --- a/gm.go +++ b/gm.go @@ -34,6 +34,7 @@ func main() { gradleBuild := args.HasGumFlag("gg") mavenBuild := args.HasGumFlag("gm") jbangBuild := args.HasGumFlag("gj") + antBuild := args.HasGumFlag("ga") version := args.HasGumFlag("gv") help := args.HasGumFlag("gh") @@ -49,6 +50,7 @@ func main() { if help { fmt.Println("Usage of gm:") + fmt.Println(" -ga\tforce Ant build") fmt.Println(" -gb\tforce Bach build") fmt.Println(" -gc\tdisplays current configuration and quits") fmt.Println(" -gd\tdisplays debug information") @@ -76,9 +78,12 @@ func main() { if bachBuild { count = count + 1 } + if antBuild { + count = count + 1 + } if count > 1 { - fmt.Println("You cannot define -gb, -gg, -gm, or -gj flags at the same time") + fmt.Println("You cannot define -gb, -gg, -gm, gj, or -ga flags at the same time") os.Exit(-1) } @@ -90,6 +95,8 @@ func main() { gum.FindJbang(gum.NewDefaultContext(true), &args).Execute() } else if bachBuild { gum.FindBach(gum.NewDefaultContext(true), &args).Execute() + } else if antBuild { + gum.FindAnt(gum.NewDefaultContext(true), &args).Execute() } else { gum.FindTool(&args) } diff --git a/gum/ant.go b/gum/ant.go new file mode 100644 index 0000000..72618b7 --- /dev/null +++ b/gum/ant.go @@ -0,0 +1,237 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright 2020-2021 Andres Almiray. +// +// 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 gum + +import ( + "errors" + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" +) + +// AntCommand defines an executable Ant command +type AntCommand struct { + context Context + config *Config + rootdir string + executable string + args *ParsedArgs + buildFile string + explicitBuildFile string +} + +// Execute executes the given command +func (c AntCommand) Execute() { + c.doConfigureAnt() + c.doExecuteAnt() +} + +func (c *AntCommand) doConfigureAnt() { + args := make([]string, 0) + + banner := make([]string, 0) + banner = append(banner, "Using Ant at '"+c.executable+"'") + + debug := c.args.HasGumFlag("gd") + + if debug { + c.config.setDebug(debug) + } + c.debugConfig() + oargs := c.args.Args + + if len(c.explicitBuildFile) > 0 { + args = append(args, "-f") + args = append(args, c.explicitBuildFile) + banner = append(banner, "to run buildFile '"+c.explicitBuildFile+"':") + } else if len(c.buildFile) > 0 { + args = append(args, "-f") + args = append(args, c.buildFile) + banner = append(banner, "to run buildFile '"+c.buildFile+"':") + } + + args = appendSafe(args, c.args.Tool) + args = append(args, "-Dbasedir="+c.rootdir) + c.args.Args = appendSafe(args, oargs) + + c.debugAnt(c.config, oargs) + + if !c.config.general.quiet { + fmt.Println(strings.Join(banner, " ")) + } +} + +func (c *AntCommand) doExecuteAnt() { + cmd := exec.Command(c.executable, c.args.Args...) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + cmd.Run() +} + +func (c *AntCommand) debugConfig() { + if c.args.HasGumFlag("gc") { + c.config.print() + os.Exit(0) + } +} + +func (c *AntCommand) debugAnt(config *Config, oargs []string) { + if c.config.general.debug { + fmt.Println("rootdir = ", c.rootdir) + fmt.Println("executable = ", c.executable) + fmt.Println("buildFile = ", c.buildFile) + fmt.Println("explicitBuildFile = ", c.explicitBuildFile) + fmt.Println("original args = ", oargs) + fmt.Println("actual args = ", c.args.Args) + fmt.Println("") + } +} + +// FindAnt finds and executes Ant +func FindAnt(context Context, args *ParsedArgs) *AntCommand { + pwd := context.GetWorkingDir() + + ant, noAnt := findAntExec(context) + explicitBuildFileSet, explicitBuildFile := findExplicitAntBuildFile(args) + buildFile, noBuildFile := findAntBuildFile(context, pwd) + + rootdir := resolveAntRootDir(context, explicitBuildFile, buildFile) + config := ReadConfig(context, rootdir) + quiet := args.HasGumFlag("gq") + + if quiet { + config.setQuiet(quiet) + } + + var executable string + if noAnt == nil { + executable = ant + } else { + warnNoAnt(context, config) + + if context.IsExplicit() { + context.Exit(-1) + } + return nil + } + + if explicitBuildFileSet { + return &AntCommand{ + context: context, + config: config, + executable: executable, + args: args, + explicitBuildFile: explicitBuildFile} + } + + if noBuildFile != nil { + if context.IsExplicit() { + fmt.Println("No Ant project found") + fmt.Println() + context.Exit(-1) + } + return nil + } + + return &AntCommand{ + context: context, + config: config, + rootdir: rootdir, + executable: executable, + args: args, + buildFile: buildFile} +} + +func resolveAntRootDir(context Context, + explicitBuildFile string, + buildFile string) string { + + if context.FileExists(explicitBuildFile) { + return filepath.Dir(explicitBuildFile) + } + return filepath.Dir(buildFile) +} + +func warnNoAnt(context Context, config *Config) { + if !config.general.quiet && context.IsExplicit() { + fmt.Printf("No %s found in path. Please install Ant.", resolveAntExec(context)) + fmt.Println() + fmt.Println("(https://ant.apache.org/bindownload.cgi)") + fmt.Println() + } +} + +// Finds the ant executable +func findAntExec(context Context) (string, error) { + ant := resolveAntExec(context) + paths := context.GetPaths() + + for i := range paths { + name := filepath.Join(paths[i], ant) + if context.FileExists(name) { + return filepath.Abs(name) + } + } + + return "", errors.New(ant + " not found") +} + +func findExplicitAntBuildFile(args *ParsedArgs) (bool, string) { + found, file, shrunkArgs := findFlagValue("-f", args.Tool) + args.Tool = shrunkArgs + if !found { + found, file, shrunkArgs = findFlagValue("-file", args.Tool) + args.Tool = shrunkArgs + } + if !found { + found, file, shrunkArgs = findFlagValue("-buildfile", args.Tool) + args.Tool = shrunkArgs + } + + if found { + file, _ = filepath.Abs(file) + return true, file + } + + return false, "" +} + +// Finds the nearest build.xml +func findAntBuildFile(context Context, dir string) (string, error) { + parentdir := filepath.Join(dir, "..") + + if parentdir == dir { + return "", errors.New("Did not find build.xml") + } + + path := filepath.Join(dir, "build.xml") + if context.FileExists(path) { + return filepath.Abs(path) + } + + return findAntBuildFile(context, parentdir) +} + +// Resolves the ant executable (OS dependent) +func resolveAntExec(context Context) string { + if context.IsWindows() { + return "ant.bat" + } + return "ant" +} diff --git a/gum/ant_test.go b/gum/ant_test.go new file mode 100644 index 0000000..2de4736 --- /dev/null +++ b/gum/ant_test.go @@ -0,0 +1,159 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright 2020-2021 Andres Almiray. +// +// 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 gum + +import ( + "path/filepath" + "testing" +) + +func TestAntSingle(t *testing.T) { + // given: + bin, _ := filepath.Abs(filepath.Join("..", "tests", "ant", "bin")) + pwd, _ := filepath.Abs(filepath.Join("..", "tests", "ant", "single")) + + context := testContext{ + quiet: true, + explicit: true, + windows: false, + workingDir: pwd, + paths: []string{bin}} + + // when: + args := ParseArgs([]string{"-gq"}) + cmd := FindAnt(context, &args) + + // then: + if cmd == nil { + t.Error("Expected a command but got nil") + return + } + + var checks = []struct { + title, actual, expected string + }{ + {"Executable", cmd.executable, filepath.Join(bin, "ant")}, + {"BuildFile", cmd.buildFile, filepath.Join(pwd, "build.xml")}, + {"ExplicitBuildFile", cmd.explicitBuildFile, ""}, + } + + for _, check := range checks { + if check.actual != check.expected { + t.Errorf("%s: got %s, want %s", check.title, check.actual, check.expected) + } + } +} + +func TestAntParent(t *testing.T) { + // given: + bin, _ := filepath.Abs(filepath.Join("..", "tests", "ant", "bin")) + pwd, _ := filepath.Abs(filepath.Join("..", "tests", "ant", "parent", "child")) + + context := testContext{ + quiet: true, + explicit: true, + windows: false, + workingDir: pwd, + paths: []string{bin}} + + // when: + args := ParseArgs([]string{"-gq"}) + cmd := FindAnt(context, &args) + + // then: + if cmd == nil { + t.Error("Expected a command but got nil") + return + } + + var checks = []struct { + title, actual, expected string + }{ + {"Executable", cmd.executable, filepath.Join(bin, "ant")}, + {"BuildFile", cmd.buildFile, filepath.Join(pwd, "build.xml")}, + {"ExplicitBuildFile", cmd.explicitBuildFile, ""}, + } + + for _, check := range checks { + if check.actual != check.expected { + t.Errorf("%s: got %s, want %s", check.title, check.actual, check.expected) + } + } +} + +func TestAntWithExplicitBuildFile(t *testing.T) { + // given: + bin, _ := filepath.Abs(filepath.Join("..", "tests", "ant", "bin")) + pwd, _ := filepath.Abs(filepath.Join("..", "tests", "ant", "parent-with-explicit", "child")) + + context := testContext{ + quiet: true, + explicit: true, + windows: false, + workingDir: pwd, + paths: []string{bin}} + + // when: + args := ParseArgs([]string{"-gq", "-f", filepath.Join(pwd, "explicit.xml")}) + cmd := FindAnt(context, &args) + + // then: + if cmd == nil { + t.Error("Expected a command but got nil") + return + } + + var checks = []struct { + title, actual, expected string + }{ + {"Executable", cmd.executable, filepath.Join(bin, "ant")}, + {"BuildFile", cmd.buildFile, ""}, + {"ExplicitBuildFile", cmd.explicitBuildFile, filepath.Join(pwd, "explicit.xml")}, + } + + for _, check := range checks { + if check.actual != check.expected { + t.Errorf("%s: got %s, want %s", check.title, check.actual, check.expected) + } + } + + cmd.doConfigureAnt() + if cmd.args.Args[0] != "-f" || cmd.args.Args[1] != filepath.Join(pwd, "explicit.xml") { + t.Errorf("args: invalid build file") + } +} + +func TestAntWithoutExecutables(t *testing.T) { + // given: + pwd, _ := filepath.Abs(filepath.Join("..", "tests", "ant", "single")) + + context := testContext{ + quiet: true, + explicit: true, + windows: false, + workingDir: pwd, + paths: []string{}} + + // when: + args := ParseArgs([]string{"-gq"}) + cmd := FindAnt(context, &args) + + // then: + if cmd != nil { + t.Error("Expected a nil command but got something") + } +} diff --git a/gum/config.go b/gum/config.go index 196a1fd..163ff5d 100644 --- a/gum/config.go +++ b/gum/config.go @@ -190,7 +190,7 @@ func (g *general) merge(other *general) { g.debug = other.d.WithMaybeAsFalse() } - if len(g.discovery) != 3 && other != nil { + if len(g.discovery) != 5 && other != nil { g.discovery = other.discovery } } diff --git a/gum/flag.go b/gum/flag.go index d9666bb..a640395 100644 --- a/gum/flag.go +++ b/gum/flag.go @@ -33,7 +33,7 @@ func (a *ParsedArgs) HasGumFlag(flag string) bool { return ok } -var gumFlags = []string{"gb", "gc", "gd", "gg", "gh", "gj", "gm", "gn", "gq", "gr", "gv"} +var gumFlags = []string{"ga", "gb", "gc", "gd", "gg", "gh", "gj", "gm", "gn", "gq", "gr", "gv"} // ParseArgs parses input args and separates them between Gum, Tool, and Args func ParseArgs(args []string) ParsedArgs { diff --git a/gum/gradle.go b/gum/gradle.go index 82be649..6aaad42 100644 --- a/gum/gradle.go +++ b/gum/gradle.go @@ -319,6 +319,7 @@ func resolveGradleWrapperExecutable(context Context, args *ParsedArgs) (string, func warnNoGradleWrapper(context Context, config *Config) { if !config.general.quiet && context.IsExplicit() { fmt.Printf("No %s set up for this project. ", resolveGradleWrapperExec(context)) + fmt.Println() fmt.Println("Please consider setting one up.") fmt.Println("(https://gradle.org/docs/current/userguide/gradle_wrapper.html)") fmt.Println() @@ -328,6 +329,7 @@ func warnNoGradleWrapper(context Context, config *Config) { func warnNoGradle(context Context, config *Config) { if !config.general.quiet && context.IsExplicit() { fmt.Printf("No %s found in path. Please install Gradle.", resolveGradleExec(context)) + fmt.Println() fmt.Println("(https://gradle.org/docs/current/userguide/installation.html)") fmt.Println() } diff --git a/gum/jbang.go b/gum/jbang.go index 2358118..35985e1 100644 --- a/gum/jbang.go +++ b/gum/jbang.go @@ -183,6 +183,7 @@ func resolveJbangRootDir(context Context, func warnNoJbangWrapper(context Context, config *Config) { if !config.general.quiet && context.IsExplicit() { fmt.Printf("No %s set up for this project. ", resolveJbangWrapperExec(context)) + fmt.Println() fmt.Println("Please consider setting one up.") fmt.Println("(https://github.com/jbangdev)") fmt.Println() @@ -192,6 +193,7 @@ func warnNoJbangWrapper(context Context, config *Config) { func warnNoJbang(context Context, config *Config) { if !config.general.quiet && context.IsExplicit() { fmt.Printf("No %s found in path. Please install jbang.", resolveJbangExec(context)) + fmt.Println() fmt.Println("(https://github.com/jbangdev)") fmt.Println() } diff --git a/gum/maven.go b/gum/maven.go index 4b70a91..03c177c 100644 --- a/gum/maven.go +++ b/gum/maven.go @@ -209,6 +209,7 @@ func resolveMavenRootDir(context Context, func warnNoMavenWrapper(context Context, config *Config) { if !config.general.quiet && context.IsExplicit() { fmt.Printf("No %s set up for this project. ", resolveMavenWrapperExec(context)) + fmt.Println() fmt.Println("Please consider setting one up.") fmt.Println("(https://maven.apache.org/)") fmt.Println() @@ -218,6 +219,7 @@ func warnNoMavenWrapper(context Context, config *Config) { func warnNoMaven(context Context, config *Config) { if !config.general.quiet && context.IsExplicit() { fmt.Printf("No %s found in path. Please install Maven.", resolveMavenExec(context)) + fmt.Println() fmt.Println("(https://maven.apache.org/download.cgi)") fmt.Println() } diff --git a/gum/tool.go b/gum/tool.go index f79a18a..6ef49fd 100644 --- a/gum/tool.go +++ b/gum/tool.go @@ -22,26 +22,27 @@ import ( "strings" ) -// FindTool Executes gradle/maven/jbang based on config discovery +// FindTool Executes gradle/maven/ant/bach/jbang based on config discovery func FindTool(args *ParsedArgs) { context := NewDefaultContext(false) config := ReadUserConfig(context) config.merge(nil) - if len(config.general.discovery) == 4 { + if len(config.general.discovery) == 5 { discoverTool(config, context, args) } doFindGradle(context, args) doFindMaven(context, args) - doFindJbang(context, args) + doFindAnt(context, args) doFindBach(context, args) + doFindJbang(context, args) if args.HasGumFlag("gc") { config.print() os.Exit(0) } else { - fmt.Println("Did not find a Gradle, Maven, Bach, or JBang project") + fmt.Println("Did not find a Gradle, Maven, Bach, JBang or Ant project") os.Exit(-1) } } @@ -63,6 +64,9 @@ func discoverTool(config *Config, context Context, args *ParsedArgs) { case "bach": doFindBach(context, args) break + case "ant": + doFindAnt(context, args) + break default: fmt.Println("Unsupported tool: " + tool) os.Exit(-1) @@ -101,3 +105,11 @@ func doFindBach(context Context, args *ParsedArgs) { os.Exit(0) } } + +func doFindAnt(context Context, args *ParsedArgs) { + ant := FindAnt(context, args) + if ant != nil { + ant.Execute() + os.Exit(0) + } +} diff --git a/tests/ant/bin/ant b/tests/ant/bin/ant new file mode 100644 index 0000000..e69de29 diff --git a/tests/ant/parent-with-explicit/build.xml b/tests/ant/parent-with-explicit/build.xml new file mode 100644 index 0000000..e69de29 diff --git a/tests/ant/parent-with-explicit/child/build.xml b/tests/ant/parent-with-explicit/child/build.xml new file mode 100644 index 0000000..e69de29 diff --git a/tests/ant/parent-with-explicit/child/explicit.xml b/tests/ant/parent-with-explicit/child/explicit.xml new file mode 100644 index 0000000..e69de29 diff --git a/tests/ant/parent/build.xml b/tests/ant/parent/build.xml new file mode 100644 index 0000000..e69de29 diff --git a/tests/ant/parent/child/build.xml b/tests/ant/parent/child/build.xml new file mode 100644 index 0000000..e69de29 diff --git a/tests/ant/single/build.xml b/tests/ant/single/build.xml new file mode 100644 index 0000000..e69de29