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
+}