Skip to content

add gradle tasks for metalava generation #671

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 15 commits into from
Aug 14, 2019
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,19 @@
package com.google.firebase.gradle.plugins;

import com.android.build.gradle.LibraryExtension;
import com.android.build.gradle.api.AndroidSourceSet;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.firebase.gradle.plugins.apiinfo.GenerateApiTxtFileTask;
import com.google.firebase.gradle.plugins.apiinfo.ApiInformationTask;
import com.google.firebase.gradle.plugins.ci.device.FirebaseTestServer;

import org.gradle.api.Plugin;
import org.gradle.api.Project;
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile;
import java.io.File;
import java.util.ArrayList;
import java.util.List;

public class FirebaseLibraryPlugin implements Plugin<Project> {

Expand Down Expand Up @@ -54,6 +61,40 @@ public void apply(Project project) {
}
});
}
if (System.getenv().containsKey("METALAVA_BINARY_PATH")) {
String METALAVA_BINARY_PATH = System.getenv("METALAVA_BINARY_PATH");
AndroidSourceSet mainSourceSet = android.getSourceSets().getByName("main");
List<String> sourcePaths = new ArrayList<>();
for (File directory : mainSourceSet.getJava().getSrcDirs()) {
sourcePaths.add(directory.getAbsolutePath());
}
String sourcePathArgument = String.join(",", sourcePaths);
project.getTasks().create("apiInformation", ApiInformationTask.class, task -> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should prefer .register() instead of .create() as it is evaluated lazily.

task.setProperty("apiTxt", project.getProjectDir() + "/api.txt");
task.setProperty("metalavaBinaryPath", METALAVA_BINARY_PATH);
task.setProperty("sourcePath", sourcePathArgument);
task.setProperty("outputPath", project.getRootProject().getBuildDir() + "/apiinfo/subproject" + project.getPath());
task.setProperty("baselinePath", project.getProjectDir() + "/baseline.txt");
if (project.hasProperty("updateBaseline")) {
task.setProperty("updateBaseline", true);
} else {
task.setProperty("updateBaseline", false);
}
});

project.getTasks().create("generateApiTxtFile", GenerateApiTxtFileTask.class, task -> {
task.setProperty("apiTxt", project.getProjectDir() + "/api.txt");
task.setProperty("metalavaBinaryPath", METALAVA_BINARY_PATH);
task.setProperty("sourcePath", sourcePathArgument);
task.setProperty("baselinePath", project.getProjectDir() + "/baseline.txt");
if (project.hasProperty("updateBaseline")) {
task.setProperty("updateBaseline", true);
} else {
task.setProperty("updateBaseline", false);
}
});
}


android.testServer(new FirebaseTestServer(project, firebaseLibrary.testLab));

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
// Copyright 2019 Google LLC
//
// 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
//
// http://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 com.google.firebase.gradle.plugins.apiinfo;

import groovy.transform.CompileStatic;
import jdk.nashorn.internal.objects.annotations.Property;
import org.gradle.api.DefaultTask;
import org.gradle.api.tasks.TaskAction;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStreamReader;

/**
Task generates the api diff of the current source code against the api.txt file stored
alongside the project's src directory.
*/
@CompileStatic
public class ApiInformationTask extends DefaultTask {

@Property
String metalavaBinaryPath;

@Property
String apiTxt;

@Property
String sourcePath;

@Property
String baselinePath;

@Property
boolean updateBaseline;

@Property
String outputPath;


private String getCmdOutput (String cmdToRun) {
ProcessBuilder processBuilder = new ProcessBuilder(cmdToRun.split(" "));
try {
Process p = processBuilder.start();
BufferedReader reader =
new BufferedReader(new InputStreamReader(p.getInputStream()));
StringBuilder builder = new StringBuilder();
String line = null;
while ( (line = reader.readLine()) != null) {
builder.append(line);
builder.append(System.getProperty("line.separator"));
}
return builder.toString();
} catch (IOException e) {
e.printStackTrace();
return null;
}

}

private void writeToFile(String output) {
try(BufferedWriter out = new BufferedWriter(new FileWriter(outputPath))) {
out.write(output);
} catch (IOException e) {
System.err.println("Error: " + e.getMessage());
}
}

@TaskAction
void execute() {
String cmdTemplate = "%s --source-path %s --check-compatibility:api:current %s --format=v2 --baseline %s --no-color";
if(updateBaseline) {
cmdTemplate = "%s --source-path %s --check-compatibility:api:current %s --format=v2 --update-baseline %s --no-color";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO, there is a lot of boilerplate here that comes down to 2 commands and their respective arguments.

%s --source-path %s --api %s --format=v2 --baseline %s
 "%s --source-path %s --check-compatibility:api:current %s --format=v2 --update-baseline %s --no-color";

I think we can remove some cruft by modelling this as

// This task executes in 2 modes.
// 1. Generate api text mode
// 2. Compare api text mode 
public abstract class MetalavaExecutionTask extends DefaultTask {
    @Input
    abstract String getMetalavaBinaryPath();

    @InputFile
    abstract File getApiTxt();

    @Input
    abstract String getSourcePath();

    @InputFile
    abstract File getBaselineFile();

    @Input
    abstract boolean getUpdateBaseline();

    @Input
    abstract enum getMode();


    @OutputFile
    abstract File getOutputFile();

    abstract void setSourcePath(String value);

    abstract void setBaselineFile(File value);

    abstract void setUpdateBaseline(boolean value);

    abstract void setMetalavaBinaryPath(String value);

    abstract void setApiTxt(File value);

    abstract void setOutputFile(File value);

    abstract void setMode(Mode mode);

    private static enum Mode {
        GENERATE, COMPARE
    }

    @TaskAction
    void execute() {
        File outputFileDir = getOutputFile().getParentFile();
        if(!outputFileDir.exists()) {
            outputFileDir.mkdirs();
        }
        
        String cmd = mode == Mode.GENERATE ? "--check-compatibility:api:current %s" : "--api %s"
        String baseline = "--baseline %s" : "--update-baseline %s"
        String format = "--format=v2"
        String sourcePath = "--source-path %s"

        String cmdTemplate = ["%s", sourcePath, cmd, baselineArg, "--no-color"].join(" ")

        String cmdToRun = String.format(cmdTemplate, getMetalavaBinaryPath(), getSourcePath(), getApiTxt().getAbsolutePath(), getBaselineFile().getAbsolutePath());

        getProject().exec(spec-> {
            spec.setCommandLine(Arrays.asList(cmdToRun.split(" ")));
            try {
                spec.setStandardOutput(new FileOutputStream(getOutputFile()));
            } catch (FileNotFoundException e) {
                getLogger().error(e.toString());
            }
        });


        
    }

}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in the ci command we would need to run the task to getApiInformation
and then run the task to generateApiInformation in this order
so we would need two tasks. However we can merge the two task classes into one though

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And there is no purpose of outputFile in the generateApiInformation Task

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The structure of the second task has changed now. It involves producing the api.txt file and then comparing the already existing api.txt with that. I feel now the structure of the two tasks are fairly different

}
String cmdToRun = String.format(cmdTemplate, metalavaBinaryPath, sourcePath, apiTxt, baselinePath);
String cmdOutput = getCmdOutput(cmdToRun);
if(cmdOutput == null ){
System.out.println("Unable to run the command " + cmdToRun);
}
else {
writeToFile(cmdOutput);
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// Copyright 2019 Google LLC
//
// 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
//
// http://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 com.google.firebase.gradle.plugins.apiinfo;

import groovy.transform.CompileStatic;
import org.gradle.api.DefaultTask;
import org.gradle.api.tasks.Input;
import org.gradle.api.tasks.TaskAction;

import java.io.IOException;

/**
Task generates the api txt file for the current source code.
*/
@CompileStatic
class GenerateApiTask extends DefaultTask {

@Input
private String metalavaBinaryPath;

@Input
private String apiTxt;

@Input
private String sourcePath;


@TaskAction
void execute() {
String cmdTemplate = "%s --source-path %s --api %s --format=v2";
String cmdToRun = String.format(cmdTemplate, metalavaBinaryPath, sourcePath, apiTxt);
try {
Runtime.getRuntime().exec(cmdToRun);
} catch (IOException e) {
System.out.println("Failed to run command " + cmdToRun);
System.out.println(e.toString());
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
// Copyright 2019 Google LLC
//
// 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
//
// http://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 com.google.firebase.gradle.plugins.apiinfo;



import org.gradle.api.DefaultTask;
import org.gradle.api.tasks.Input;
import org.gradle.api.tasks.TaskAction;

import java.io.IOException;

import groovy.transform.CompileStatic;
import jdk.nashorn.internal.objects.annotations.Property;

@CompileStatic
public class GenerateApiTxtFileTask extends DefaultTask {

@Property
String metalavaBinaryPath;

@Property
String apiTxt;

@Property
String sourcePath;

@Property
String baselinePath;

@Property
boolean updateBaseline;



@TaskAction
void execute() {
String cmdTemplate = "%s --source-path %s --api %s --format=v2 --baseline %s";
if(updateBaseline) {
cmdTemplate = "%s --source-path %s --api %s --format=v2 --update-baseline %s";
}
String cmdToRun = String.format(cmdTemplate, metalavaBinaryPath, sourcePath, apiTxt, baselinePath);
try {
Process p = Runtime.getRuntime().exec(cmdToRun);
System.out.println("Generated api txt file at " + apiTxt);
} catch (IOException e) {
System.out.println("Failed to run command " + cmdToRun);
System.out.println(e.toString());
}
}

}
37 changes: 37 additions & 0 deletions ci/fireci/fireci/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
import click
import os

from github import Github

from . import gradle
from . import ci_command
from . import stats
Expand Down Expand Up @@ -61,3 +63,38 @@ def smoke_tests(app_build_variant, test_apps_dir):
os.path.join(cwd, 'build', 'm2repository')),
workdir=location,
)


@click.argument('issue_number', required=True)
@click.argument('repo_name', required=True)
@click.argument('auth_token', required=True)
@ci_command()
def api_information(auth_token, repo_name, issue_number):
"""Comments the api information on the pr"""

gradle.run('apiInformation')
gradle.run('generateApiTxtFile')
dir_suffix = 'build/apiinfo'
comment_string = ""
for filename in os.listdir(dir_suffix):
subproject = filename
formatted_output_lines = []
with open(os.path.join(dir_suffix, filename), 'r') as f:
outputlines = f.readlines()
for line in outputlines:
if 'error' in line:
formatted_output_lines.append(line[line.find('error:'):])
if formatted_output_lines:
comment_string += 'The public api surface has changed for the subproject {}:\n'.format(subproject[len('subproject:'):])
comment_string += ''.join(formatted_output_lines)
comment_string += '\n'
if comment_string:
comment_string += ('Please update the api.txt files for the subprojects being affected by this change '
'with the files present in the artifacts directory. Also perform a major/minor bump accordingly.\n')
else:
comment_string = 'No project\'s public api surface was changed due to this PR.\n'
# Comment to github.
github_client = Github(auth_token)
repo = github_client.get_repo(repo_name)
pr = repo.get_pull(int(issue_number))
pr.create_issue_comment(comment_string)
1 change: 1 addition & 0 deletions ci/fireci/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
'click==7.0',
'opencensus==0.2.0',
'google-cloud-monitoring==0.31.1',
'PyGithub==1.43.8'
],
packages=find_packages(exclude=['tests']),
entry_points={
Expand Down
1 change: 1 addition & 0 deletions firebase-abt/api.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
// Signature format: 2.0
1 change: 1 addition & 0 deletions firebase-abt/baseline.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
// Baseline format: 1.0
Loading