devtools/madb: embed the gradle init script in the madb binary.

The madb_init.gradle file is now embedded in the madb binary, to make
it easier for the people who are not Vanadium contributors to use madb
more easily.

A test is added to make sure that the embedded script always matches
with the madb_init.gradle source file.

Change-Id: Idcf348c138893a7595ba496bb93c9c63e73b93ee
diff --git a/embedded_gradle.go b/embedded_gradle.go
new file mode 100644
index 0000000..f51a71a
--- /dev/null
+++ b/embedded_gradle.go
@@ -0,0 +1,209 @@
+// Copyright 2016 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// This file was auto-generated via go generate.
+// DO NOT UPDATE MANUALLY
+
+package main
+
+const gradleInitScript = `// Copyright 2016 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+import groovy.json.*;
+
+allprojects {
+
+    // Add the extract task only to the project in the current directory.
+    if (project.projectDir == gradle.startParameter.currentDir) {
+        task madbExtractVariantProperties << {
+            extract(project)
+        }
+    }
+}
+
+// Main driver of the property extraction script.
+void extract(project) {
+    project = getApplicationModule(project)
+    if (project == null) {
+        def errMsg = 'The current project is not an Android application module, '
+            + 'nor does it contain any application sub-modules. '
+            + 'Please run the madb command from an Android application project directory.'
+        throw new GradleException(errMsg)
+    }
+
+    // Get the target variant.
+    def targetVariant = getTargetVariant(project)
+
+    // Collect the variant properties in a map, so that it can be printed out as a JSON.
+    def result = [
+        ProjectPath:    project.path,
+        VariantName:    targetVariant.name,
+        CleanTask:      project.path + ":clean",
+        AssembleTask:   targetVariant.assemble.path,
+        AppID:          getApplicationId(targetVariant),
+        Activity:       getMainActivity(project),
+        AbiFilters:     getAbiFilters(targetVariant),
+        VariantOutputs: getVariantOutputs(targetVariant)
+    ]
+
+    // Format the resulting map into JSON and print it.
+    def resultJson = JsonOutput.prettyPrint(JsonOutput.toJson(result))
+    printResult(project, resultJson)
+}
+
+// Prints the given result to the desired output stream.
+// If the output file is specified, write the result to the file.
+// Otherwise, print it out to the console.
+void printResult(project, result) {
+    // An optional output file name can be specified by a Gradle command-line flag:
+    // -PmadbOutputFile=<file_name>
+    def output = project.properties.containsKey('madbOutputFile')
+        ? new File(project.properties['madbOutputFile'])
+        : null
+
+    if (output != null) {
+        // Empty the file, and then print the result.
+        output.text = ''
+        output.append(result)
+    } else {
+        println result
+    }
+}
+
+// Returns an Android application module from the given project.
+// The given project is returned immediately, if itself is an application module.
+// Otherwise, the first available application sub-module is returned, if any.
+// Returns null if no Android application modules were found from the given project.
+Object getApplicationModule(project) {
+    if (isApplicationModule(project)) {
+        return project
+    }
+
+    def subApplicationModules = project.subprojects.findAll { isApplicationModule(it) }
+    if (subApplicationModules.isEmpty()) {
+        return null
+    }
+
+    def result = subApplicationModules.first()
+    if (subApplicationModules.size() > 1) {
+        print 'Multiple application sub-modules were detected. '
+        println 'The first application module "' + result.name + '" is chosen automatically.'
+        println '(NOTE: Application module can be explicitly specified using -module=<name> flag.)'
+    }
+
+    return result
+}
+
+// Returns true iff the given project is an Android application.
+boolean isApplicationModule(project) {
+    return project.plugins.hasPlugin('com.android.application')
+}
+
+// Returns the target application variant for the project.
+// If the 'madbVariant' property was explicitly set from the command line, the
+// matching variant is returned.
+// If there is no variant with the provided name, it throws an exception.
+//
+// If the 'madbVariant' property is not provided, the first available variant is
+// returned. Usually the first available variant would be 'debug'.
+Object getTargetVariant(project) {
+    def allVariants = project.android.applicationVariants
+
+    if (project.properties.containsKey('madbVariant')) {
+        def variantName = project.properties['madbVariant']
+        def targetVariant = allVariants.find { variantName.equalsIgnoreCase(it.name) }
+        if (targetVariant == null) {
+            throw new GradleException('Variant "' + variantName + '" is not found.')
+        }
+
+        return targetVariant
+    } else {
+        def targetVariant = allVariants.iterator().next()
+        print 'Build variant not specified. '
+        println 'The first variant "' + targetVariant.name + '" is chosen automatically.'
+        println '(NOTE: Variant can be explicitly specified using -variant=<name> flag.)'
+
+        return targetVariant
+    }
+}
+
+// Returns the application ID for the given variant.
+String getApplicationId(variant) {
+    def suffix = variant.buildType.applicationIdSuffix
+    if (suffix == null) {
+        suffix = ""
+    }
+
+    return variant.mergedFlavor.applicationId + suffix
+}
+
+String getMainActivity(project) {
+    def manifestFile = getAndroidManifestLocation(project)
+
+    // Parse the xml file and find the main activity.
+    def manifest = new XmlSlurper().parse(manifestFile)
+    def mainActivity = manifest.application.activity.find { isMainActivity(it) }
+    def name = mainActivity.'@android:name'.text()
+
+    // If the activity name is using the shorthand syntax starting with a dot,
+    // make it a fully-qualified name by prepending it with the package name.
+    if (name.startsWith('.')) {
+        return manifest.'@package'.text() + name
+    } else {
+        return name
+    }
+}
+
+// Returns the location of the "AndroidManifest.xml" file.
+// TODO(youngseokyoon): investigate whether we can obtain the merged manifest.
+File getAndroidManifestLocation(project) {
+    try {
+        return project.android.sourceSets.main.manifest.srcFile
+    } catch (all) {
+        return null
+    }
+}
+
+// Determines whether the given activity is the main activity or not.
+boolean isMainActivity(activity) {
+    try {
+        def intentFilter = activity.'intent-filter'
+        return  intentFilter.action.'@android:name'.text() == 'android.intent.action.MAIN' &&
+                intentFilter.category.'@android:name'.text() == 'android.intent.category.LAUNCHER'
+    } catch (all) {
+        return false
+    }
+}
+
+// Returns the list of supported ABIs for the given variant.
+// Returns null if there are no ABI filters specified.
+Object getAbiFilters(variant) {
+    return variant.variantData.variantConfiguration.supportedAbis
+}
+
+// Gets the outputs and their properties of the given variant.
+// The returned object is a list of variant outputs, each of which is a map containing the
+// properties of a variant output, such as the absolute path of the .apk file, and its filters.
+Object getVariantOutputs(variant) {
+    def variantOutputs = []
+    for (def variantOutput : variant.outputs) {
+        def filters = []
+        for (def filter : variantOutput.mainOutputFile.filters) {
+            filters.add([FilterType: filter.filterType, Identifier: filter.identifier])
+        }
+
+        def result = [
+            Name: variantOutput.name,
+            OutputFilePath: variantOutput.mainOutputFile.outputFile.absolutePath,
+            VersionCode: variantOutput.versionCode,
+            Filters: filters
+        ]
+
+        variantOutputs.add(result)
+    }
+
+    return variantOutputs
+}
+`
diff --git a/madb.go b/madb.go
index 59e60fe..ae9648f 100644
--- a/madb.go
+++ b/madb.go
@@ -4,6 +4,7 @@
 
 // The following enables go generate to generate the doc.go file.
 //go:generate go run $JIRI_ROOT/release/go/src/v.io/x/lib/cmdline/testdata/gendoc.go .
+//go:generate go run testdata/embed_gradle_script.go madb_init.gradle embedded_gradle.go gradleInitScript
 
 package main
 
@@ -572,21 +573,6 @@
 	return "", fmt.Errorf("Could not find the Gradle wrapper in dir %q or its parent directories.", dir)
 }
 
-// TODO(youngseokyoon): find a better way to distribute the gradle script.
-func findGradleInitScript() (string, error) {
-	jiriRoot := os.Getenv("JIRI_ROOT")
-	if jiriRoot == "" {
-		return "", fmt.Errorf("JIRI_ROOT environment variable is not set")
-	}
-
-	initScript := filepath.Join(jiriRoot, "release", "go", "src", "v.io", "x", "devtools", "madb", "madb_init.gradle")
-	if _, err := os.Stat(initScript); err != nil {
-		return "", err
-	}
-
-	return initScript, nil
-}
-
 func extractPropertiesFromGradle(key variantKey) (variantProperties, error) {
 	sh := gosh.NewShell(nil)
 	defer sh.Cleanup()
@@ -602,16 +588,16 @@
 		return variantProperties{}, err
 	}
 
-	initScript, err := findGradleInitScript()
-	if err != nil {
-		return variantProperties{}, fmt.Errorf("Could not find the madb_init.gradle script: %v", err)
-	}
+	// Write the init script in a temp file.
+	initScript := sh.MakeTempFile()
+	initScript.WriteString(gradleInitScript)
+	initScript.Close()
 
 	// Create a temporary file in which Gradle can write the results.
 	outputFile := sh.MakeTempFile()
 
 	// Run the gradle wrapper to extract the application ID and the main activity name from the build scripts.
-	cmdArgs := []string{"--daemon", "-q", "-I", initScript, "-PmadbOutputFile=" + outputFile.Name()}
+	cmdArgs := []string{"--daemon", "-q", "-I", initScript.Name(), "-PmadbOutputFile=" + outputFile.Name()}
 
 	// Specify the project directory. If the module name is explicitly set, combine it with the base directory.
 	cmdArgs = append(cmdArgs, "-p", filepath.Join(key.Dir, key.Module))
diff --git a/madb_init.gradle b/madb_init.gradle
index 1c97a4a..7d7879c 100644
--- a/madb_init.gradle
+++ b/madb_init.gradle
@@ -1,3 +1,7 @@
+// Copyright 2016 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
 import groovy.json.*;
 
 allprojects {
@@ -176,16 +180,18 @@
 Object getVariantOutputs(variant) {
     def variantOutputs = []
     for (def variantOutput : variant.outputs) {
-        def result = [:]
-        result['Name'] = variantOutput.name
-        result['OutputFilePath'] = variantOutput.mainOutputFile.outputFile.absolutePath
-        result['VersionCode'] = variantOutput.versionCode
-
         def filters = []
         for (def filter : variantOutput.mainOutputFile.filters) {
             filters.add([FilterType: filter.filterType, Identifier: filter.identifier])
         }
-        result['Filters'] = filters
+
+        def result = [
+            Name: variantOutput.name,
+            OutputFilePath: variantOutput.mainOutputFile.outputFile.absolutePath,
+            VersionCode: variantOutput.versionCode,
+            Filters: filters
+        ]
+
         variantOutputs.add(result)
     }
 
diff --git a/madb_test.go b/madb_test.go
index 826e8e1..65016b4 100644
--- a/madb_test.go
+++ b/madb_test.go
@@ -372,3 +372,22 @@
 		t.Fatalf("unmatched results: got %v, want %v", got, want)
 	}
 }
+
+// TestEmbeddedGradleScript tests whether the gradle script defined in embedded_gradle.go matches
+// the madb_init.gradle file.
+func TestEmbeddedGradleScript(t *testing.T) {
+	f, err := os.Open("madb_init.gradle")
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer f.Close()
+
+	bytes, err := ioutil.ReadAll(f)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	if string(bytes) != gradleInitScript {
+		t.Fatalf(`The embedded Gradle script is out of date. Please run "jiri go generate" to regenerate the embedded script.`)
+	}
+}
diff --git a/testdata/embed_gradle_script.go b/testdata/embed_gradle_script.go
new file mode 100644
index 0000000..d383438
--- /dev/null
+++ b/testdata/embed_gradle_script.go
@@ -0,0 +1,86 @@
+// Copyright 2016 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// A script that takes a source Gradle script, and writes a .go file that contains a constant string
+// variable that holds the contents of the source script. By doing this, the Gradle script can be
+// embedded in the madb binary. This script is located under testdata, to avoid being installed in
+// the $GOPATH/bin directory.
+//
+// This script is meant to be run via go generate from the parent directory.
+// See the go:generate comment at the top of madb.go file.
+package main
+
+import (
+	"fmt"
+	"io/ioutil"
+	"os"
+	"text/template"
+)
+
+const (
+	tmpl = `// Copyright 2016 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// This file was auto-generated via go generate.
+// DO NOT UPDATE MANUALLY
+
+package main
+
+const {{.VarName}} = {{.Backtick}}{{.Contents}}{{.Backtick}}
+`
+)
+
+func main() {
+	if len(os.Args) != 4 {
+		fmt.Println("Usage: go run embed_gradle_script.go <source file path> <destination .go file path> <variable name>")
+		os.Exit(1)
+	}
+
+	if err := generateEmbeddedScript(os.Args[1], os.Args[2], os.Args[3]); err != nil {
+		fmt.Fprintf(os.Stderr, "Failed to generate the embedded script: %v", err)
+		os.Exit(1)
+	}
+}
+
+func generateEmbeddedScript(source, dest, varName string) error {
+	// Read the source file.
+	srcFile, err := os.Open(source)
+	if err != nil {
+		return err
+	}
+	defer srcFile.Close()
+
+	bytes, err := ioutil.ReadAll(srcFile)
+	if err != nil {
+		return err
+	}
+
+	// Create the destination file.
+	destFile, err := os.Create(dest)
+	if err != nil {
+		return err
+	}
+	defer destFile.Close()
+
+	// Load the template.
+	t, err := template.New("embedded_gradle").Parse(tmpl)
+	if err != nil {
+		return err
+	}
+
+	// Define the data to be used within the template.
+	data := map[string]string{
+		"VarName":  varName,
+		"Contents": string(bytes),
+		"Backtick": "`",
+	}
+
+	// Execute the template with the above data.
+	if err := t.Execute(destFile, data); err != nil {
+		return err
+	}
+
+	return nil
+}