Add support for running Android lint on java and android modules.

Add a rule that runs Android lint on each java and android module
and produces reports in xml, html and text formats.

Bug: 153485543
Test: m out/soong/.intermediates/packages/apps/Settings/Settings-core/android_common/lint-report.html
Change-Id: I5a530975b73ba767fef45b257d4f9ec901a19fcb
Merged-In: I5a530975b73ba767fef45b257d4f9ec901a19fcb
(cherry picked from commit 014489c1e6)
This commit is contained in:
Colin Cross 2020-06-02 20:09:13 -07:00
parent 1c14b4ecf6
commit 1e28e3c615
11 changed files with 710 additions and 0 deletions

View file

@ -37,6 +37,7 @@ bootstrap_go_package {
"jdeps.go",
"java_resources.go",
"kotlin.go",
"lint.go",
"platform_compat_config.go",
"plugin.go",
"prebuilt_apis.go",

View file

@ -102,6 +102,7 @@ type aapt struct {
sdkLibraries []string
hasNoCode bool
LoggingParent string
resourceFiles android.Paths
splitNames []string
splits []split
@ -275,6 +276,7 @@ func (a *aapt) buildActions(ctx android.ModuleContext, sdkContext sdkContext, ex
var compiledResDirs []android.Paths
for _, dir := range resDirs {
a.resourceFiles = append(a.resourceFiles, dir.files...)
compiledResDirs = append(compiledResDirs, aapt2Compile(ctx, dir.dir, dir.files, compileFlags).Paths())
}
@ -473,6 +475,10 @@ func (a *AndroidLibrary) GenerateAndroidBuildActions(ctx android.ModuleContext)
// apps manifests are handled by aapt, don't let Module see them
a.properties.Manifest = nil
a.linter.mergedManifest = a.aapt.mergedManifestFile
a.linter.manifest = a.aapt.manifestPath
a.linter.resources = a.aapt.resourceFiles
a.Module.extraProguardFlagFiles = append(a.Module.extraProguardFlagFiles,
a.proguardOptionsFile)
@ -512,6 +518,7 @@ func AndroidLibraryFactory() android.Module {
&module.androidLibraryProperties)
module.androidLibraryProperties.BuildAAR = true
module.Module.linter.library = true
android.InitApexModule(module)
InitJavaModule(module, android.DeviceSupported)

View file

@ -737,6 +737,10 @@ func (a *AndroidApp) generateAndroidBuildActions(ctx android.ModuleContext) {
a.proguardBuildActions(ctx)
a.linter.mergedManifest = a.aapt.mergedManifestFile
a.linter.manifest = a.aapt.manifestPath
a.linter.resources = a.aapt.resourceFiles
dexJarFile := a.dexBuildActions(ctx)
jniLibs, certificateDeps := collectAppDeps(ctx, a, a.shouldEmbedJnis(ctx), !Bool(a.appProperties.Jni_uses_platform_apis))
@ -1089,6 +1093,7 @@ func AndroidTestFactory() android.Module {
module.appProperties.Use_embedded_native_libs = proptools.BoolPtr(true)
module.appProperties.AlwaysPackageNativeLibs = true
module.Module.dexpreopter.isTest = true
module.Module.linter.test = true
module.addHostAndDeviceProperties()
module.AddProperties(
@ -1138,6 +1143,7 @@ func AndroidTestHelperAppFactory() android.Module {
module.appProperties.Use_embedded_native_libs = proptools.BoolPtr(true)
module.appProperties.AlwaysPackageNativeLibs = true
module.Module.dexpreopter.isTest = true
module.Module.linter.test = true
module.addHostAndDeviceProperties()
module.AddProperties(

View file

@ -1224,6 +1224,21 @@ func DroidstubsHostFactory() android.Module {
return module
}
func (d *Droidstubs) OutputFiles(tag string) (android.Paths, error) {
switch tag {
case "":
return android.Paths{d.stubsSrcJar}, nil
case ".docs.zip":
return android.Paths{d.docZip}, nil
case ".annotations.zip":
return android.Paths{d.annotationsZip}, nil
case ".api_versions.xml":
return android.Paths{d.apiVersionsXml}, nil
default:
return nil, fmt.Errorf("unsupported module reference tag %q", tag)
}
}
func (d *Droidstubs) ApiFilePath() android.Path {
return d.apiFilePath
}

View file

@ -475,6 +475,7 @@ type Module struct {
hiddenAPI
dexpreopter
linter
// list of the xref extraction files
kytheFiles android.Paths
@ -494,6 +495,7 @@ func (j *Module) addHostAndDeviceProperties() {
j.AddProperties(
&j.deviceProperties,
&j.dexpreoptProperties,
&j.linter.properties,
)
}
@ -1635,6 +1637,28 @@ func (j *Module) compile(ctx android.ModuleContext, aaptSrcJar android.Path) {
outputFile = implementationAndResourcesJar
}
if ctx.Device() {
lintSDKVersionString := func(sdkSpec sdkSpec) string {
if v := sdkSpec.version; v.isNumbered() {
return v.String()
} else {
return ctx.Config().DefaultAppTargetSdk()
}
}
j.linter.name = ctx.ModuleName()
j.linter.srcs = srcFiles
j.linter.srcJars = srcJars
j.linter.classpath = append(append(android.Paths(nil), flags.bootClasspath...), flags.classpath...)
j.linter.classes = j.implementationJarFile
j.linter.minSdkVersion = lintSDKVersionString(j.minSdkVersion())
j.linter.targetSdkVersion = lintSDKVersionString(j.targetSdkVersion())
j.linter.compileSdkVersion = lintSDKVersionString(j.sdkVersion())
j.linter.javaLanguageLevel = flags.javaVersion.String()
j.linter.kotlinLanguageLevel = "1.3"
j.linter.lint(ctx)
}
ctx.CheckbuildFile(outputFile)
// Save the output file with no relative path so that it doesn't end up in a subdirectory when used as a resource
@ -2235,6 +2259,7 @@ func TestFactory() android.Module {
module.Module.properties.Installable = proptools.BoolPtr(true)
module.Module.dexpreopter.isTest = true
module.Module.linter.test = true
InitJavaModule(module, android.HostAndDeviceSupported)
return module
@ -2249,6 +2274,7 @@ func TestHelperLibraryFactory() android.Module {
module.Module.properties.Installable = proptools.BoolPtr(true)
module.Module.dexpreopter.isTest = true
module.Module.linter.test = true
InitJavaModule(module, android.HostAndDeviceSupported)
return module
@ -2823,6 +2849,7 @@ func DefaultsFactory() android.Module {
&DexImportProperties{},
&android.ApexProperties{},
&RuntimeResourceOverlayProperties{},
&LintProperties{},
)
android.InitDefaultsModule(module)

355
java/lint.go Normal file
View file

@ -0,0 +1,355 @@
// Copyright 2020 Google Inc. All rights reserved.
//
// 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 java
import (
"fmt"
"sort"
"android/soong/android"
)
type LintProperties struct {
// Controls for running Android Lint on the module.
Lint struct {
// If true, run Android Lint on the module. Defaults to true.
Enabled *bool
// Flags to pass to the Android Lint tool.
Flags []string
// Checks that should be treated as fatal.
Fatal_checks []string
// Checks that should be treated as errors.
Error_checks []string
// Checks that should be treated as warnings.
Warning_checks []string
// Checks that should be skipped.
Disabled_checks []string
}
}
type linter struct {
name string
manifest android.Path
mergedManifest android.Path
srcs android.Paths
srcJars android.Paths
resources android.Paths
classpath android.Paths
classes android.Path
extraLintCheckJars android.Paths
test bool
library bool
minSdkVersion string
targetSdkVersion string
compileSdkVersion string
javaLanguageLevel string
kotlinLanguageLevel string
outputs lintOutputs
properties LintProperties
}
type lintOutputs struct {
html android.ModuleOutPath
text android.ModuleOutPath
xml android.ModuleOutPath
}
func (l *linter) enabled() bool {
return BoolDefault(l.properties.Lint.Enabled, true)
}
func (l *linter) writeLintProjectXML(ctx android.ModuleContext,
rule *android.RuleBuilder) (projectXMLPath, configXMLPath, cacheDir android.WritablePath, deps android.Paths) {
var resourcesList android.WritablePath
if len(l.resources) > 0 {
// The list of resources may be too long to put on the command line, but
// we can't use the rsp file because it is already being used for srcs.
// Insert a second rule to write out the list of resources to a file.
resourcesList = android.PathForModuleOut(ctx, "lint", "resources.list")
resListRule := android.NewRuleBuilder()
resListRule.Command().Text("cp").FlagWithRspFileInputList("", l.resources).Output(resourcesList)
resListRule.Build(pctx, ctx, "lint_resources_list", "lint resources list")
deps = append(deps, l.resources...)
}
projectXMLPath = android.PathForModuleOut(ctx, "lint", "project.xml")
// Lint looks for a lint.xml file next to the project.xml file, give it one.
configXMLPath = android.PathForModuleOut(ctx, "lint", "lint.xml")
cacheDir = android.PathForModuleOut(ctx, "lint", "cache")
srcJarDir := android.PathForModuleOut(ctx, "lint-srcjars")
srcJarList := zipSyncCmd(ctx, rule, srcJarDir, l.srcJars)
cmd := rule.Command().
BuiltTool(ctx, "lint-project-xml").
FlagWithOutput("--project_out ", projectXMLPath).
FlagWithOutput("--config_out ", configXMLPath).
FlagWithArg("--name ", ctx.ModuleName())
if l.library {
cmd.Flag("--library")
}
if l.test {
cmd.Flag("--test")
}
if l.manifest != nil {
deps = append(deps, l.manifest)
cmd.FlagWithArg("--manifest ", l.manifest.String())
}
if l.mergedManifest != nil {
deps = append(deps, l.mergedManifest)
cmd.FlagWithArg("--merged_manifest ", l.mergedManifest.String())
}
// TODO(ccross): some of the files in l.srcs are generated sources and should be passed to
// lint separately.
cmd.FlagWithRspFileInputList("--srcs ", l.srcs)
deps = append(deps, l.srcs...)
cmd.FlagWithInput("--generated_srcs ", srcJarList)
deps = append(deps, l.srcJars...)
if resourcesList != nil {
cmd.FlagWithInput("--resources ", resourcesList)
}
if l.classes != nil {
deps = append(deps, l.classes)
cmd.FlagWithArg("--classes ", l.classes.String())
}
cmd.FlagForEachArg("--classpath ", l.classpath.Strings())
deps = append(deps, l.classpath...)
cmd.FlagForEachArg("--extra_checks_jar ", l.extraLintCheckJars.Strings())
deps = append(deps, l.extraLintCheckJars...)
// The cache tag in project.xml is relative to the project.xml file.
cmd.FlagWithArg("--cache_dir ", "cache")
cmd.FlagWithInput("@",
android.PathForSource(ctx, "build/soong/java/lint_defaults.txt"))
cmd.FlagForEachArg("--disable_check ", l.properties.Lint.Disabled_checks)
cmd.FlagForEachArg("--warning_check ", l.properties.Lint.Warning_checks)
cmd.FlagForEachArg("--error_check ", l.properties.Lint.Error_checks)
cmd.FlagForEachArg("--fatal_check ", l.properties.Lint.Fatal_checks)
return projectXMLPath, configXMLPath, cacheDir, deps
}
// generateManifest adds a command to the rule to write a dummy manifest cat contains the
// minSdkVersion and targetSdkVersion for modules (like java_library) that don't have a manifest.
func (l *linter) generateManifest(ctx android.ModuleContext, rule *android.RuleBuilder) android.Path {
manifestPath := android.PathForModuleOut(ctx, "lint", "AndroidManifest.xml")
rule.Command().Text("(").
Text(`echo "<?xml version='1.0' encoding='utf-8'?>" &&`).
Text(`echo "<manifest xmlns:android='http://schemas.android.com/apk/res/android'" &&`).
Text(`echo " android:versionCode='1' android:versionName='1' >" &&`).
Textf(`echo " <uses-sdk android:minSdkVersion='%s' android:targetSdkVersion='%s'/>" &&`,
l.minSdkVersion, l.targetSdkVersion).
Text(`echo "</manifest>"`).
Text(") >").Output(manifestPath)
return manifestPath
}
func (l *linter) lint(ctx android.ModuleContext) {
if !l.enabled() {
return
}
rule := android.NewRuleBuilder()
if l.manifest == nil {
manifest := l.generateManifest(ctx, rule)
l.manifest = manifest
}
projectXML, lintXML, cacheDir, deps := l.writeLintProjectXML(ctx, rule)
l.outputs.html = android.PathForModuleOut(ctx, "lint-report.html")
l.outputs.text = android.PathForModuleOut(ctx, "lint-report.txt")
l.outputs.xml = android.PathForModuleOut(ctx, "lint-report.xml")
rule.Command().Text("rm -rf").Flag(cacheDir.String())
rule.Command().Text("mkdir -p").Flag(cacheDir.String())
rule.Command().
Text("(").
Flag("JAVA_OPTS=-Xmx2048m").
FlagWithInput("SDK_ANNOTATIONS=", annotationsZipPath(ctx)).
FlagWithInput("LINT_OPTS=-DLINT_API_DATABASE=", apiVersionsXmlPath(ctx)).
Tool(android.PathForSource(ctx, "prebuilts/cmdline-tools/tools/bin/lint")).
Implicit(android.PathForSource(ctx, "prebuilts/cmdline-tools/tools/lib/lint-classpath.jar")).
Flag("--quiet").
FlagWithInput("--project ", projectXML).
FlagWithInput("--config ", lintXML).
FlagWithOutput("--html ", l.outputs.html).
FlagWithOutput("--text ", l.outputs.text).
FlagWithOutput("--xml ", l.outputs.xml).
FlagWithArg("--compile-sdk-version ", l.compileSdkVersion).
FlagWithArg("--java-language-level ", l.javaLanguageLevel).
FlagWithArg("--kotlin-language-level ", l.kotlinLanguageLevel).
FlagWithArg("--url ", fmt.Sprintf(".=.,%s=out", android.PathForOutput(ctx).String())).
Flag("--exitcode").
Flags(l.properties.Lint.Flags).
Implicits(deps).
Text("|| (").Text("cat").Input(l.outputs.text).Text("; exit 7)").
Text(")")
rule.Command().Text("rm -rf").Flag(cacheDir.String())
rule.Build(pctx, ctx, "lint", "lint")
}
func (l *linter) lintOutputs() *lintOutputs {
return &l.outputs
}
type lintOutputIntf interface {
lintOutputs() *lintOutputs
}
var _ lintOutputIntf = (*linter)(nil)
type lintSingleton struct {
htmlZip android.WritablePath
textZip android.WritablePath
xmlZip android.WritablePath
}
func (l *lintSingleton) GenerateBuildActions(ctx android.SingletonContext) {
l.generateLintReportZips(ctx)
l.copyLintDependencies(ctx)
}
func (l *lintSingleton) copyLintDependencies(ctx android.SingletonContext) {
if ctx.Config().UnbundledBuild() {
return
}
var frameworkDocStubs android.Module
ctx.VisitAllModules(func(m android.Module) {
if ctx.ModuleName(m) == "framework-doc-stubs" {
if frameworkDocStubs == nil {
frameworkDocStubs = m
} else {
ctx.Errorf("lint: multiple framework-doc-stubs modules found: %s and %s",
ctx.ModuleSubDir(m), ctx.ModuleSubDir(frameworkDocStubs))
}
}
})
if frameworkDocStubs == nil {
if !ctx.Config().AllowMissingDependencies() {
ctx.Errorf("lint: missing framework-doc-stubs")
}
return
}
ctx.Build(pctx, android.BuildParams{
Rule: android.Cp,
Input: android.OutputFileForModule(ctx, frameworkDocStubs, ".annotations.zip"),
Output: annotationsZipPath(ctx),
})
ctx.Build(pctx, android.BuildParams{
Rule: android.Cp,
Input: android.OutputFileForModule(ctx, frameworkDocStubs, ".api_versions.xml"),
Output: apiVersionsXmlPath(ctx),
})
}
func annotationsZipPath(ctx android.PathContext) android.WritablePath {
return android.PathForOutput(ctx, "lint", "annotations.zip")
}
func apiVersionsXmlPath(ctx android.PathContext) android.WritablePath {
return android.PathForOutput(ctx, "lint", "api_versions.xml")
}
func (l *lintSingleton) generateLintReportZips(ctx android.SingletonContext) {
var outputs []*lintOutputs
var dirs []string
ctx.VisitAllModules(func(m android.Module) {
if ctx.Config().EmbeddedInMake() && !m.ExportedToMake() {
return
}
if apex, ok := m.(android.ApexModule); ok && apex.NotAvailableForPlatform() && apex.IsForPlatform() {
// There are stray platform variants of modules in apexes that are not available for
// the platform, and they sometimes can't be built. Don't depend on them.
return
}
if l, ok := m.(lintOutputIntf); ok {
outputs = append(outputs, l.lintOutputs())
}
})
dirs = android.SortedUniqueStrings(dirs)
zip := func(outputPath android.WritablePath, get func(*lintOutputs) android.Path) {
var paths android.Paths
for _, output := range outputs {
paths = append(paths, get(output))
}
sort.Slice(paths, func(i, j int) bool {
return paths[i].String() < paths[j].String()
})
rule := android.NewRuleBuilder()
rule.Command().BuiltTool(ctx, "soong_zip").
FlagWithOutput("-o ", outputPath).
FlagWithArg("-C ", android.PathForIntermediates(ctx).String()).
FlagWithRspFileInputList("-l ", paths)
rule.Build(pctx, ctx, outputPath.Base(), outputPath.Base())
}
l.htmlZip = android.PathForOutput(ctx, "lint-report-html.zip")
zip(l.htmlZip, func(l *lintOutputs) android.Path { return l.html })
l.textZip = android.PathForOutput(ctx, "lint-report-text.zip")
zip(l.textZip, func(l *lintOutputs) android.Path { return l.text })
l.xmlZip = android.PathForOutput(ctx, "lint-report-xml.zip")
zip(l.xmlZip, func(l *lintOutputs) android.Path { return l.xml })
ctx.Phony("lint-check", l.htmlZip, l.textZip, l.xmlZip)
}
func (l *lintSingleton) MakeVars(ctx android.MakeVarsContext) {
ctx.DistForGoal("lint-check", l.htmlZip, l.textZip, l.xmlZip)
}
var _ android.SingletonMakeVarsProvider = (*lintSingleton)(nil)
func init() {
android.RegisterSingletonType("lint",
func() android.Singleton { return &lintSingleton{} })
}

78
java/lint_defaults.txt Normal file
View file

@ -0,0 +1,78 @@
# Treat LintError as fatal to catch invocation errors
--fatal_check LintError
# Downgrade existing errors to warnings
--warning_check AppCompatResource # 55 occurences in 10 modules
--warning_check AppLinkUrlError # 111 occurences in 53 modules
--warning_check BlockedPrivateApi # 2 occurences in 2 modules
--warning_check ByteOrderMark # 2 occurences in 2 modules
--warning_check DuplicateActivity # 3 occurences in 3 modules
--warning_check DuplicateDefinition # 3623 occurences in 48 modules
--warning_check DuplicateIds # 207 occurences in 22 modules
--warning_check EllipsizeMaxLines # 12 occurences in 7 modules
--warning_check ExtraTranslation # 21276 occurences in 27 modules
--warning_check FontValidationError # 4 occurences in 1 modules
--warning_check FullBackupContent # 16 occurences in 1 modules
--warning_check GetContentDescriptionOverride # 3 occurences in 2 modules
--warning_check HalfFloat # 31 occurences in 1 modules
--warning_check HardcodedDebugMode # 99 occurences in 95 modules
--warning_check ImpliedQuantity # 703 occurences in 27 modules
--warning_check ImpliedTouchscreenHardware # 4 occurences in 4 modules
--warning_check IncludeLayoutParam # 11 occurences in 6 modules
--warning_check Instantiatable # 145 occurences in 19 modules
--warning_check InvalidPermission # 6 occurences in 4 modules
--warning_check InvalidUsesTagAttribute # 6 occurences in 2 modules
--warning_check InvalidWakeLockTag # 111 occurences in 37 modules
--warning_check JavascriptInterface # 3 occurences in 2 modules
--warning_check LibraryCustomView # 9 occurences in 4 modules
--warning_check LogTagMismatch # 81 occurences in 13 modules
--warning_check LongLogTag # 249 occurences in 12 modules
--warning_check MenuTitle # 5 occurences in 4 modules
--warning_check MissingClass # 537 occurences in 141 modules
--warning_check MissingConstraints # 39 occurences in 10 modules
--warning_check MissingDefaultResource # 1257 occurences in 40 modules
--warning_check MissingIntentFilterForMediaSearch # 1 occurences in 1 modules
--warning_check MissingLeanbackLauncher # 3 occurences in 3 modules
--warning_check MissingLeanbackSupport # 2 occurences in 2 modules
--warning_check MissingOnPlayFromSearch # 1 occurences in 1 modules
--warning_check MissingPermission # 2071 occurences in 150 modules
--warning_check MissingPrefix # 46 occurences in 41 modules
--warning_check MissingQuantity # 100 occurences in 1 modules
--warning_check MissingSuperCall # 121 occurences in 36 modules
--warning_check MissingTvBanner # 3 occurences in 3 modules
--warning_check NamespaceTypo # 3 occurences in 3 modules
--warning_check NetworkSecurityConfig # 46 occurences in 12 modules
--warning_check NewApi # 1996 occurences in 122 modules
--warning_check NotSibling # 15 occurences in 10 modules
--warning_check ObjectAnimatorBinding # 14 occurences in 5 modules
--warning_check OnClick # 49 occurences in 21 modules
--warning_check Orientation # 77 occurences in 19 modules
--warning_check Override # 385 occurences in 36 modules
--warning_check ParcelCreator # 23 occurences in 2 modules
--warning_check ProtectedPermissions # 2413 occurences in 381 modules
--warning_check Range # 80 occurences in 28 modules
--warning_check RecyclerView # 1 occurences in 1 modules
--warning_check ReferenceType # 4 occurences in 1 modules
--warning_check ResourceAsColor # 19 occurences in 14 modules
--warning_check RequiredSize # 52 occurences in 13 modules
--warning_check ResAuto # 3 occurences in 1 modules
--warning_check ResourceCycle # 37 occurences in 10 modules
--warning_check ResourceType # 137 occurences in 36 modules
--warning_check RestrictedApi # 28 occurences in 5 modules
--warning_check RtlCompat # 9 occurences in 6 modules
--warning_check ServiceCast # 3 occurences in 1 modules
--warning_check SoonBlockedPrivateApi # 5 occurences in 3 modules
--warning_check StringFormatInvalid # 148 occurences in 11 modules
--warning_check StringFormatMatches # 4800 occurences in 30 modules
--warning_check UnknownId # 8 occurences in 7 modules
--warning_check ValidFragment # 12 occurences in 5 modules
--warning_check ValidRestrictions # 5 occurences in 1 modules
--warning_check WebViewLayout # 3 occurences in 1 modules
--warning_check WrongCall # 21 occurences in 3 modules
--warning_check WrongConstant # 894 occurences in 126 modules
--warning_check WrongManifestParent # 10 occurences in 4 modules
--warning_check WrongThread # 14 occurences in 6 modules
--warning_check WrongViewCast # 1 occurences in 1 modules
# TODO(b/158390965): remove this when lint doesn't crash
--disable_check HardcodedDebugMode

View file

@ -221,6 +221,7 @@ func RobolectricTestFactory() android.Module {
&module.robolectricProperties)
module.Module.dexpreopter.isTest = true
module.Module.linter.test = true
InitJavaModule(module, android.DeviceSupported)
return module

View file

@ -1101,6 +1101,7 @@ func (module *SdkLibrary) createImplLibrary(mctx android.DefaultableHookContext)
&module.protoProperties,
&module.deviceProperties,
&module.dexpreoptProperties,
&module.linter.properties,
&props,
module.sdkComponentPropertiesForChildLibrary(),
}

View file

@ -148,3 +148,9 @@ python_test_host {
],
test_suites: ["general-tests"],
}
python_binary_host {
name: "lint-project-xml",
main: "lint-project-xml.py",
srcs: ["lint-project-xml.py"],
}

213
scripts/lint-project-xml.py Executable file
View file

@ -0,0 +1,213 @@
#!/usr/bin/env python3
#
# Copyright (C) 2018 The Android Open Source Project
#
# 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.
#
"""This file generates project.xml and lint.xml files used to drive the Android Lint CLI tool."""
import argparse
def check_action(check_type):
"""
Returns an action that appends a tuple of check_type and the argument to the dest.
"""
class CheckAction(argparse.Action):
def __init__(self, option_strings, dest, nargs=None, **kwargs):
if nargs is not None:
raise ValueError("nargs must be None, was %s" % nargs)
super(CheckAction, self).__init__(option_strings, dest, **kwargs)
def __call__(self, parser, namespace, values, option_string=None):
checks = getattr(namespace, self.dest, [])
checks.append((check_type, values))
setattr(namespace, self.dest, checks)
return CheckAction
def parse_args():
"""Parse commandline arguments."""
def convert_arg_line_to_args(arg_line):
for arg in arg_line.split():
if arg.startswith('#'):
return
if not arg.strip():
continue
yield arg
parser = argparse.ArgumentParser(fromfile_prefix_chars='@')
parser.convert_arg_line_to_args = convert_arg_line_to_args
parser.add_argument('--project_out', dest='project_out',
help='file to which the project.xml contents will be written.')
parser.add_argument('--config_out', dest='config_out',
help='file to which the lint.xml contents will be written.')
parser.add_argument('--name', dest='name',
help='name of the module.')
parser.add_argument('--srcs', dest='srcs', action='append', default=[],
help='file containing whitespace separated list of source files.')
parser.add_argument('--generated_srcs', dest='generated_srcs', action='append', default=[],
help='file containing whitespace separated list of generated source files.')
parser.add_argument('--resources', dest='resources', action='append', default=[],
help='file containing whitespace separated list of resource files.')
parser.add_argument('--classes', dest='classes', action='append', default=[],
help='file containing the module\'s classes.')
parser.add_argument('--classpath', dest='classpath', action='append', default=[],
help='file containing classes from dependencies.')
parser.add_argument('--extra_checks_jar', dest='extra_checks_jars', action='append', default=[],
help='file containing extra lint checks.')
parser.add_argument('--manifest', dest='manifest',
help='file containing the module\'s manifest.')
parser.add_argument('--merged_manifest', dest='merged_manifest',
help='file containing merged manifest for the module and its dependencies.')
parser.add_argument('--library', dest='library', action='store_true',
help='mark the module as a library.')
parser.add_argument('--test', dest='test', action='store_true',
help='mark the module as a test.')
parser.add_argument('--cache_dir', dest='cache_dir',
help='directory to use for cached file.')
group = parser.add_argument_group('check arguments', 'later arguments override earlier ones.')
group.add_argument('--fatal_check', dest='checks', action=check_action('fatal'), default=[],
help='treat a lint issue as a fatal error.')
group.add_argument('--error_check', dest='checks', action=check_action('error'), default=[],
help='treat a lint issue as an error.')
group.add_argument('--warning_check', dest='checks', action=check_action('warning'), default=[],
help='treat a lint issue as a warning.')
group.add_argument('--disable_check', dest='checks', action=check_action('ignore'), default=[],
help='disable a lint issue.')
return parser.parse_args()
class NinjaRspFileReader:
"""
Reads entries from a Ninja rsp file. Ninja escapes any entries in the file that contain a
non-standard character by surrounding the whole entry with single quotes, and then replacing
any single quotes in the entry with the escape sequence '\''.
"""
def __init__(self, filename):
self.f = open(filename, 'r')
self.r = self.character_reader(self.f)
def __iter__(self):
return self
def character_reader(self, f):
"""Turns a file into a generator that returns one character at a time."""
while True:
c = f.read(1)
if c:
yield c
else:
return
def __next__(self):
entry = self.read_entry()
if entry:
return entry
else:
raise StopIteration
def read_entry(self):
c = next(self.r, "")
if not c:
return ""
elif c == "'":
return self.read_quoted_entry()
else:
entry = c
for c in self.r:
if c == " " or c == "\n":
break
entry += c
return entry
def read_quoted_entry(self):
entry = ""
for c in self.r:
if c == "'":
# Either the end of the quoted entry, or the beginning of an escape sequence, read the next
# character to find out.
c = next(self.r)
if not c or c == " " or c == "\n":
# End of the item
return entry
elif c == "\\":
# Escape sequence, expect a '
c = next(self.r)
if c != "'":
# Malformed escape sequence
raise "malformed escape sequence %s'\\%s" % (entry, c)
entry += "'"
else:
raise "malformed escape sequence %s'%s" % (entry, c)
else:
entry += c
raise "unterminated quoted entry %s" % entry
def write_project_xml(f, args):
test_attr = "test='true' " if args.test else ""
f.write("<?xml version='1.0' encoding='utf-8'?>\n")
f.write("<project>\n")
f.write(" <module name='%s' android='true' %sdesugar='full' >\n" % (args.name, "library='true' " if args.library else ""))
if args.manifest:
f.write(" <manifest file='%s' %s/>\n" % (args.manifest, test_attr))
if args.merged_manifest:
f.write(" <merged-manifest file='%s' %s/>\n" % (args.merged_manifest, test_attr))
for src_file in args.srcs:
for src in NinjaRspFileReader(src_file):
f.write(" <src file='%s' %s/>\n" % (src, test_attr))
for src_file in args.generated_srcs:
for src in NinjaRspFileReader(src_file):
f.write(" <src file='%s' generated='true' %s/>\n" % (src, test_attr))
for res_file in args.resources:
for res in NinjaRspFileReader(res_file):
f.write(" <resource file='%s' %s/>\n" % (res, test_attr))
for classes in args.classes:
f.write(" <classes jar='%s' />\n" % classes)
for classpath in args.classpath:
f.write(" <classpath jar='%s' />\n" % classpath)
for extra in args.extra_checks_jars:
f.write(" <lint-checks jar='%s' />\n" % extra)
f.write(" </module>\n")
if args.cache_dir:
f.write(" <cache dir='%s'/>\n" % args.cache_dir)
f.write("</project>\n")
def write_config_xml(f, args):
f.write("<?xml version='1.0' encoding='utf-8'?>\n")
f.write("<lint>\n")
for check in args.checks:
f.write(" <issue id='%s' severity='%s' />\n" % (check[1], check[0]))
f.write("</lint>\n")
def main():
"""Program entry point."""
args = parse_args()
if args.project_out:
with open(args.project_out, 'w') as f:
write_project_xml(f, args)
if args.config_out:
with open(args.config_out, 'w') as f:
write_config_xml(f, args)
if __name__ == '__main__':
main()