Merge "Enforce hidden api usage in vendor (soong)"
diff --git a/Android.bp b/Android.bp
index fff17ef..1b68adb 100644
--- a/Android.bp
+++ b/Android.bp
@@ -55,8 +55,10 @@
"android/mutator.go",
"android/namespace.go",
"android/neverallow.go",
+ "android/notices.go",
"android/onceper.go",
"android/override_module.go",
+ "android/package.go",
"android/package_ctx.go",
"android/path_properties.go",
"android/paths.go",
@@ -87,6 +89,7 @@
"android/namespace_test.go",
"android/neverallow_test.go",
"android/onceper_test.go",
+ "android/package_test.go",
"android/path_properties_test.go",
"android/paths_test.go",
"android/prebuilt_test.go",
diff --git a/README.md b/README.md
index 8fdce4b..ebbe8bc 100644
--- a/README.md
+++ b/README.md
@@ -133,6 +133,25 @@
This is based on the Bazel package concept.
+The `package` module type allows information to be specified about a package. Only a single
+`package` module can be specified per package and in the case where there are multiple `.bp` files
+in the same package directory it is highly recommended that the `package` module (if required) is
+specified in the `Android.bp` file.
+
+Unlike most module type `package` does not have a `name` property. Instead the name is set to the
+name of the package, e.g. if the package is in `top/intermediate/package` then the package name is
+`//top/intermediate/package`.
+
+E.g. The following will set the default visibility for all the modules defined in the package and
+any subpackages that do not set their own default visibility (irrespective of whether they are in
+the same `.bp` file as the `package` module) to be visible to all the subpackages by default.
+
+```
+package {
+ default_visibility: [":__subpackages"]
+}
+```
+
### Name resolution
Soong provides the ability for modules in different directories to specify
@@ -191,13 +210,13 @@
`//independent:evil`)
* `["//project"]`: This is shorthand for `["//project:__pkg__"]`
* `[":__subpackages__"]`: This is shorthand for `["//project:__subpackages__"]`
-where `//project` is the module's package. e.g. using `[":__subpackages__"]` in
+where `//project` is the module's package, e.g. using `[":__subpackages__"]` in
`packages/apps/Settings/Android.bp` is equivalent to
`//packages/apps/Settings:__subpackages__`.
* `["//visibility:legacy_public"]`: The default visibility, behaves as
`//visibility:public` for now. It is an error if it is used in a module.
-The visibility rules of `//visibility:public` and `//visibility:private` can not
+The visibility rules of `//visibility:public` and `//visibility:private` cannot
be combined with any other visibility specifications, except
`//visibility:public` is allowed to override visibility specifications imported
through the `defaults` property.
@@ -207,13 +226,22 @@
say `vendor/google`, instead it must make itself visible to all packages within
`vendor/` using `//vendor:__subpackages__`.
-If a module does not specify the `visibility` property the module is
-`//visibility:legacy_public`. Once the build has been completely switched over to
-soong it is possible that a global refactoring will be done to change this to
-`//visibility:private` at which point all modules that do not currently specify
-a `visibility` property will be updated to have
-`visibility = [//visibility:legacy_public]` added. It will then be the owner's
-responsibility to replace that with a more appropriate visibility.
+If a module does not specify the `visibility` property then it uses the
+`default_visibility` property of the `package` module in the module's package.
+
+If the `default_visibility` property is not set for the module's package then
+it will use the `default_visibility` of its closest ancestor package for which
+a `default_visibility` property is specified.
+
+If no `default_visibility` property can be found then the module uses the
+global default of `//visibility:legacy_public`.
+
+Once the build has been completely switched over to soong it is possible that a
+global refactoring will be done to change this to `//visibility:private` at
+which point all packages that do not currently specify a `default_visibility`
+property will be updated to have
+`default_visibility = [//visibility:legacy_public]` added. It will then be the
+owner's responsibility to replace that with a more appropriate visibility.
### Formatter
diff --git a/android/config.go b/android/config.go
index b0d8b7f..e4012fa 100644
--- a/android/config.go
+++ b/android/config.go
@@ -216,6 +216,7 @@
AAPTPreferredConfig: stringPtr("xhdpi"),
AAPTCharacteristics: stringPtr("nosdcard"),
AAPTPrebuiltDPI: []string{"xhdpi", "xxhdpi"},
+ UncompressPrivAppDex: boolPtr(true),
},
buildDir: buildDir,
diff --git a/android/module.go b/android/module.go
index 87e2ca7..43b8763 100644
--- a/android/module.go
+++ b/android/module.go
@@ -202,6 +202,55 @@
BuildParamsForTests() []BuildParams
RuleParamsForTests() map[blueprint.Rule]blueprint.RuleParams
VariablesForTests() map[string]string
+
+ // Get the qualified module id for this module.
+ qualifiedModuleId(ctx BaseModuleContext) qualifiedModuleName
+
+ // Get information about the properties that can contain visibility rules.
+ visibilityProperties() []visibilityProperty
+}
+
+// Qualified id for a module
+type qualifiedModuleName struct {
+ // The package (i.e. directory) in which the module is defined, without trailing /
+ pkg string
+
+ // The name of the module, empty string if package.
+ name string
+}
+
+func (q qualifiedModuleName) String() string {
+ if q.name == "" {
+ return "//" + q.pkg
+ }
+ return "//" + q.pkg + ":" + q.name
+}
+
+func (q qualifiedModuleName) isRootPackage() bool {
+ return q.pkg == "" && q.name == ""
+}
+
+// Get the id for the package containing this module.
+func (q qualifiedModuleName) getContainingPackageId() qualifiedModuleName {
+ pkg := q.pkg
+ if q.name == "" {
+ if pkg == "" {
+ panic(fmt.Errorf("Cannot get containing package id of root package"))
+ }
+
+ index := strings.LastIndex(pkg, "/")
+ if index == -1 {
+ pkg = ""
+ } else {
+ pkg = pkg[:index]
+ }
+ }
+ return newPackageId(pkg)
+}
+
+func newPackageId(pkg string) qualifiedModuleName {
+ // A qualified id for a package module has no name.
+ return qualifiedModuleName{pkg: pkg, name: ""}
}
type nameProperties struct {
@@ -236,6 +285,20 @@
// //packages/apps/Settings:__subpackages__.
// ["//visibility:legacy_public"]: The default visibility, behaves as //visibility:public
// for now. It is an error if it is used in a module.
+ //
+ // If a module does not specify the `visibility` property then it uses the
+ // `default_visibility` property of the `package` module in the module's package.
+ //
+ // If a module does not specify the `visibility` property then it uses the
+ // `default_visibility` property of the `package` module in the module's package.
+ //
+ // If the `default_visibility` property is not set for the module's package then
+ // it will use the `default_visibility` of its closest ancestor package for which
+ // a `default_visibility` property is specified.
+ //
+ // If no `default_visibility` property can be found then the module uses the
+ // global default of `//visibility:legacy_public`.
+ //
// See https://android.googlesource.com/platform/build/soong/+/master/README.md#visibility for
// more details.
Visibility []string
@@ -571,6 +634,18 @@
return m
}
+func (m *ModuleBase) qualifiedModuleId(ctx BaseModuleContext) qualifiedModuleName {
+ return qualifiedModuleName{pkg: ctx.ModuleDir(), name: ctx.ModuleName()}
+}
+
+func (m *ModuleBase) visibilityProperties() []visibilityProperty {
+ return []visibilityProperty{
+ newVisibilityProperty("visibility", func() []string {
+ return m.base().commonProperties.Visibility
+ }),
+ }
+}
+
func (m *ModuleBase) SetTarget(target Target, multiTargets []Target, primary bool) {
m.commonProperties.CompileTarget = target
m.commonProperties.CompileMultiTargets = multiTargets
@@ -901,14 +976,6 @@
}
if m.Enabled() {
- m.module.GenerateAndroidBuildActions(ctx)
- if ctx.Failed() {
- return
- }
-
- m.installFiles = append(m.installFiles, ctx.installFiles...)
- m.checkbuildFiles = append(m.checkbuildFiles, ctx.checkbuildFiles...)
-
notice := proptools.StringDefault(m.commonProperties.Notice, "NOTICE")
if module := SrcIsModule(notice); module != "" {
m.noticeFile = ctx.ExpandOptionalSource(¬ice, "notice")
@@ -916,6 +983,14 @@
noticePath := filepath.Join(ctx.ModuleDir(), notice)
m.noticeFile = ExistentPathForSource(ctx, noticePath)
}
+
+ m.module.GenerateAndroidBuildActions(ctx)
+ if ctx.Failed() {
+ return
+ }
+
+ m.installFiles = append(m.installFiles, ctx.installFiles...)
+ m.checkbuildFiles = append(m.checkbuildFiles, ctx.checkbuildFiles...)
} else if ctx.Config().AllowMissingDependencies() {
// If the module is not enabled it will not create any build rules, nothing will call
// ctx.GetMissingDependencies(), and blueprint will consider the missing dependencies to be unhandled
diff --git a/android/mutator.go b/android/mutator.go
index 081c2b2..070b420 100644
--- a/android/mutator.go
+++ b/android/mutator.go
@@ -75,6 +75,8 @@
var preArch = []RegisterMutatorFunc{
registerLoadHookMutator,
RegisterNamespaceMutator,
+ // Rename package module types.
+ registerPackageRenamer,
RegisterPrebuiltsPreArchMutators,
registerVisibilityRuleChecker,
RegisterDefaultsPreArchMutators,
diff --git a/android/notices.go b/android/notices.go
new file mode 100644
index 0000000..dbb88fc
--- /dev/null
+++ b/android/notices.go
@@ -0,0 +1,87 @@
+// Copyright 2019 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 android
+
+import (
+ "path/filepath"
+
+ "github.com/google/blueprint"
+)
+
+func init() {
+ pctx.SourcePathVariable("merge_notices", "build/soong/scripts/mergenotice.py")
+ pctx.SourcePathVariable("generate_notice", "build/make/tools/generate-notice-files.py")
+
+ pctx.HostBinToolVariable("minigzip", "minigzip")
+}
+
+var (
+ mergeNoticesRule = pctx.AndroidStaticRule("mergeNoticesRule", blueprint.RuleParams{
+ Command: `${merge_notices} --output $out $in`,
+ CommandDeps: []string{"${merge_notices}"},
+ Description: "merge notice files into $out",
+ })
+
+ generateNoticeRule = pctx.AndroidStaticRule("generateNoticeRule", blueprint.RuleParams{
+ Command: `rm -rf $tmpDir $$(dirname $out) && ` +
+ `mkdir -p $tmpDir $$(dirname $out) && ` +
+ `${generate_notice} --text-output $tmpDir/NOTICE.txt --html-output $tmpDir/NOTICE.html -t "$title" -s $inputDir && ` +
+ `${minigzip} -c $tmpDir/NOTICE.html > $out`,
+ CommandDeps: []string{"${generate_notice}", "${minigzip}"},
+ Description: "produce notice file $out",
+ }, "tmpDir", "title", "inputDir")
+)
+
+func MergeNotices(ctx ModuleContext, mergedNotice WritablePath, noticePaths []Path) {
+ ctx.Build(pctx, BuildParams{
+ Rule: mergeNoticesRule,
+ Description: "merge notices",
+ Inputs: noticePaths,
+ Output: mergedNotice,
+ })
+}
+
+func BuildNoticeOutput(ctx ModuleContext, installPath OutputPath, installFilename string,
+ noticePaths []Path) ModuleOutPath {
+ // Merge all NOTICE files into one.
+ // TODO(jungjw): We should just produce a well-formatted NOTICE.html file in a single pass.
+ //
+ // generate-notice-files.py, which processes the merged NOTICE file, has somewhat strict rules
+ // about input NOTICE file paths.
+ // 1. Their relative paths to the src root become their NOTICE index titles. We want to use
+ // on-device paths as titles, and so output the merged NOTICE file the corresponding location.
+ // 2. They must end with .txt extension. Otherwise, they're ignored.
+ noticeRelPath := InstallPathToOnDevicePath(ctx, installPath.Join(ctx, installFilename+".txt"))
+ mergedNotice := PathForModuleOut(ctx, filepath.Join("NOTICE_FILES/src", noticeRelPath))
+ MergeNotices(ctx, mergedNotice, noticePaths)
+
+ // Transform the merged NOTICE file into a gzipped HTML file.
+ noticeOutput := PathForModuleOut(ctx, "NOTICE", "NOTICE.html.gz")
+ tmpDir := PathForModuleOut(ctx, "NOTICE_tmp")
+ title := "Notices for " + ctx.ModuleName()
+ ctx.Build(pctx, BuildParams{
+ Rule: generateNoticeRule,
+ Description: "generate notice output",
+ Input: mergedNotice,
+ Output: noticeOutput,
+ Args: map[string]string{
+ "tmpDir": tmpDir.String(),
+ "title": title,
+ "inputDir": PathForModuleOut(ctx, "NOTICE_FILES/src").String(),
+ },
+ })
+
+ return noticeOutput
+}
diff --git a/android/override_module.go b/android/override_module.go
index 5a57c93..22fb7de 100644
--- a/android/override_module.go
+++ b/android/override_module.go
@@ -95,8 +95,6 @@
// Base module struct for overridable module types
type OverridableModuleBase struct {
- ModuleBase
-
// List of OverrideModules that override this base module
overrides []OverrideModule
// Used to parallelize registerOverrideMutator executions. Note that only addOverride locks this
@@ -144,7 +142,7 @@
// Adds the base module to the overrides property, if exists, of the overriding module. See the
// comment on OverridableModuleBase.overridesProperty for details.
if b.overridesProperty != nil {
- *b.overridesProperty = append(*b.overridesProperty, b.Name())
+ *b.overridesProperty = append(*b.overridesProperty, ctx.ModuleName())
}
for _, p := range b.overridableProperties {
for _, op := range o.getOverridingProperties() {
diff --git a/android/package.go b/android/package.go
new file mode 100644
index 0000000..03f6a1e
--- /dev/null
+++ b/android/package.go
@@ -0,0 +1,194 @@
+// Copyright 2019 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 android
+
+import (
+ "fmt"
+ "sync/atomic"
+
+ "github.com/google/blueprint"
+)
+
+func init() {
+ RegisterModuleType("package", PackageFactory)
+}
+
+// The information maintained about each package.
+type packageInfo struct {
+ // The module from which this information was populated. If `duplicated` = true then this is the
+ // module that has been renamed and must be used to report errors.
+ module *packageModule
+
+ // If true this indicates that there are two package statements in the same package which is not
+ // allowed and will cause the build to fail. This flag is set by packageRenamer and checked in
+ // packageErrorReporter
+ duplicated bool
+}
+
+type packageProperties struct {
+ Name string `blueprint:"mutated"`
+
+ // Specifies the default visibility for all modules defined in this package.
+ Default_visibility []string
+}
+
+type packageModule struct {
+ ModuleBase
+
+ properties packageProperties
+ packageInfo *packageInfo
+}
+
+func (p *packageModule) GenerateAndroidBuildActions(ModuleContext) {
+ // Nothing to do.
+}
+
+func (p *packageModule) GenerateBuildActions(ctx blueprint.ModuleContext) {
+ // Nothing to do.
+}
+
+func (p *packageModule) qualifiedModuleId(ctx BaseModuleContext) qualifiedModuleName {
+ // Override to create a package id.
+ return newPackageId(ctx.ModuleDir())
+}
+
+// Override to ensure that the default_visibility rules are checked by the visibility module during
+// its checking phase.
+func (p *packageModule) visibilityProperties() []visibilityProperty {
+ return []visibilityProperty{
+ newVisibilityProperty("default_visibility", func() []string {
+ return p.properties.Default_visibility
+ }),
+ }
+}
+
+func (p *packageModule) Name() string {
+ return p.properties.Name
+}
+
+func (p *packageModule) setName(name string) {
+ p.properties.Name = name
+}
+
+// Counter to ensure package modules are created with a unique name within whatever namespace they
+// belong.
+var packageCount uint32 = 0
+
+func PackageFactory() Module {
+ module := &packageModule{}
+
+ // Get a unique if for the package. Has to be done atomically as the creation of the modules are
+ // done in parallel.
+ id := atomic.AddUint32(&packageCount, 1)
+ name := fmt.Sprintf("soong_package_%d", id)
+
+ module.properties.Name = name
+
+ module.AddProperties(&module.properties)
+ return module
+}
+
+// Registers the function that renames the packages.
+func registerPackageRenamer(ctx RegisterMutatorsContext) {
+ ctx.BottomUp("packageRenamer", packageRenamer).Parallel()
+ ctx.BottomUp("packageErrorReporter", packageErrorReporter).Parallel()
+}
+
+// Renames the package to match the package directory.
+//
+// This also creates a PackageInfo object for each package and uses that to detect and remember
+// duplicates for later error reporting.
+func packageRenamer(ctx BottomUpMutatorContext) {
+ m, ok := ctx.Module().(*packageModule)
+ if !ok {
+ return
+ }
+
+ packageName := "//" + ctx.ModuleDir()
+
+ pi := newPackageInfo(ctx, packageName, m)
+ if pi.module != m {
+ // Remember that the package was duplicated but do not rename as that will cause an error to
+ // be logged with the generated name. Similarly, reporting the error here will use the generated
+ // name as renames are only processed after this phase.
+ pi.duplicated = true
+ } else {
+ // This is the first package module in this package so rename it to match the package name.
+ m.setName(packageName)
+ ctx.Rename(packageName)
+
+ // Store a package info reference in the module.
+ m.packageInfo = pi
+ }
+}
+
+// Logs any deferred errors.
+func packageErrorReporter(ctx BottomUpMutatorContext) {
+ m, ok := ctx.Module().(*packageModule)
+ if !ok {
+ return
+ }
+
+ packageDir := ctx.ModuleDir()
+ packageName := "//" + packageDir
+
+ // Get the PackageInfo for the package. Should have been populated in the packageRenamer phase.
+ pi := findPackageInfo(ctx, packageName)
+ if pi == nil {
+ ctx.ModuleErrorf("internal error, expected package info to be present for package '%s'",
+ packageName)
+ return
+ }
+
+ if pi.module != m {
+ // The package module has been duplicated but this is not the module that has been renamed so
+ // ignore it. An error will be logged for the renamed module which will ensure that the error
+ // message uses the correct name.
+ return
+ }
+
+ // Check to see whether there are duplicate package modules in the package.
+ if pi.duplicated {
+ ctx.ModuleErrorf("package {...} specified multiple times")
+ return
+ }
+}
+
+type defaultPackageInfoKey string
+
+func newPackageInfo(
+ ctx BaseModuleContext, packageName string, module *packageModule) *packageInfo {
+ key := NewCustomOnceKey(defaultPackageInfoKey(packageName))
+
+ return ctx.Config().Once(key, func() interface{} {
+ return &packageInfo{module: module}
+ }).(*packageInfo)
+}
+
+// Get the PackageInfo for the package name (starts with //, no trailing /), is nil if no package
+// module type was specified.
+func findPackageInfo(ctx BaseModuleContext, packageName string) *packageInfo {
+ key := NewCustomOnceKey(defaultPackageInfoKey(packageName))
+
+ pi := ctx.Config().Once(key, func() interface{} {
+ return nil
+ })
+
+ if pi == nil {
+ return nil
+ } else {
+ return pi.(*packageInfo)
+ }
+}
diff --git a/android/package_test.go b/android/package_test.go
new file mode 100644
index 0000000..f1f47ac
--- /dev/null
+++ b/android/package_test.go
@@ -0,0 +1,111 @@
+package android
+
+import (
+ "io/ioutil"
+ "os"
+ "testing"
+)
+
+var packageTests = []struct {
+ name string
+ fs map[string][]byte
+ expectedErrors []string
+}{
+ // Package default_visibility handling is tested in visibility_test.go
+ {
+ name: "package must not accept visibility and name properties",
+ fs: map[string][]byte{
+ "top/Blueprints": []byte(`
+ package {
+ name: "package",
+ visibility: ["//visibility:private"],
+ }`),
+ },
+ expectedErrors: []string{
+ `top/Blueprints:3:10: mutated field name cannot be set in a Blueprint file`,
+ `top/Blueprints:4:16: unrecognized property "visibility"`,
+ },
+ },
+ {
+ name: "multiple packages in separate directories",
+ fs: map[string][]byte{
+ "top/Blueprints": []byte(`
+ package {
+ }`),
+ "other/Blueprints": []byte(`
+ package {
+ }`),
+ "other/nested/Blueprints": []byte(`
+ package {
+ }`),
+ },
+ },
+ {
+ name: "package must not be specified more than once per package",
+ fs: map[string][]byte{
+ "top/Blueprints": []byte(`
+ package {
+ default_visibility: ["//visibility:private"],
+ }
+
+ package {
+ }`),
+ },
+ expectedErrors: []string{
+ `module "//top": package {...} specified multiple times`,
+ },
+ },
+}
+
+func TestPackage(t *testing.T) {
+ buildDir, err := ioutil.TempDir("", "soong_package_test")
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer os.RemoveAll(buildDir)
+
+ for _, test := range packageTests {
+ t.Run(test.name, func(t *testing.T) {
+ _, errs := testPackage(buildDir, test.fs)
+
+ expectedErrors := test.expectedErrors
+ if expectedErrors == nil {
+ FailIfErrored(t, errs)
+ } else {
+ for _, expectedError := range expectedErrors {
+ FailIfNoMatchingErrors(t, expectedError, errs)
+ }
+ if len(errs) > len(expectedErrors) {
+ t.Errorf("additional errors found, expected %d, found %d", len(expectedErrors), len(errs))
+ for i, expectedError := range expectedErrors {
+ t.Errorf("expectedErrors[%d] = %s", i, expectedError)
+ }
+ for i, err := range errs {
+ t.Errorf("errs[%d] = %s", i, err)
+ }
+ }
+ }
+ })
+ }
+}
+
+func testPackage(buildDir string, fs map[string][]byte) (*TestContext, []error) {
+
+ // Create a new config per test as visibility information is stored in the config.
+ config := TestArchConfig(buildDir, nil)
+
+ ctx := NewTestArchContext()
+ ctx.RegisterModuleType("package", ModuleFactoryAdaptor(PackageFactory))
+ ctx.PreArchMutators(registerPackageRenamer)
+ ctx.Register()
+
+ ctx.MockFileSystem(fs)
+
+ _, errs := ctx.ParseBlueprintsFiles(".")
+ if len(errs) > 0 {
+ return ctx, errs
+ }
+
+ _, errs = ctx.PrepareBuildActions(config)
+ return ctx, errs
+}
diff --git a/android/visibility.go b/android/visibility.go
index c7ef1da..94af343 100644
--- a/android/visibility.go
+++ b/android/visibility.go
@@ -23,14 +23,21 @@
// Enforces visibility rules between modules.
//
-// Two stage process:
-// * First stage works bottom up to extract visibility information from the modules, parse it,
+// Multi stage process:
+// * First stage works bottom up, before defaults expansion, to check the syntax of the visibility
+// rules that have been specified.
+//
+// * Second stage works bottom up to extract the package info for each package and store them in a
+// map by package name. See package.go for functionality for this.
+//
+// * Third stage works bottom up to extract visibility information from the modules, parse it,
// create visibilityRule structures and store them in a map keyed by the module's
// qualifiedModuleName instance, i.e. //<pkg>:<name>. The map is stored in the context rather
// than a global variable for testing. Each test has its own Config so they do not share a map
-// and so can be run in parallel.
+// and so can be run in parallel. If a module has no visibility specified then it uses the
+// default package visibility if specified.
//
-// * Second stage works top down and iterates over all the deps for each module. If the dep is in
+// * Fourth stage works top down and iterates over all the deps for each module. If the dep is in
// the same package then it is automatically visible. Otherwise, for each dep it first extracts
// its visibilityRule from the config map. If one could not be found then it assumes that it is
// publicly visible. Otherwise, it calls the visibility rule to check that the module can see
@@ -48,19 +55,6 @@
var visibilityRuleRegexp = regexp.MustCompile(visibilityRulePattern)
-// Qualified id for a module
-type qualifiedModuleName struct {
- // The package (i.e. directory) in which the module is defined, without trailing /
- pkg string
-
- // The name of the module.
- name string
-}
-
-func (q qualifiedModuleName) String() string {
- return fmt.Sprintf("//%s:%s", q.pkg, q.name)
-}
-
// A visibility rule is associated with a module and determines which other modules it is visible
// to, i.e. which other modules can depend on the rule's module.
type visibilityRule interface {
@@ -71,6 +65,32 @@
String() string
}
+// Describes the properties provided by a module that contain visibility rules.
+type visibilityPropertyImpl struct {
+ name string
+ stringsGetter func() []string
+}
+
+type visibilityProperty interface {
+ getName() string
+ getStrings() []string
+}
+
+func newVisibilityProperty(name string, stringsGetter func() []string) visibilityProperty {
+ return visibilityPropertyImpl{
+ name: name,
+ stringsGetter: stringsGetter,
+ }
+}
+
+func (p visibilityPropertyImpl) getName() string {
+ return p.name
+}
+
+func (p visibilityPropertyImpl) getStrings() []string {
+ return p.stringsGetter()
+}
+
// A compositeRule is a visibility rule composed from a list of atomic visibility rules.
//
// The list corresponds to the list of strings in the visibility property after defaults expansion.
@@ -96,9 +116,9 @@
return false
}
-func (r compositeRule) String() string {
- s := make([]string, 0, len(r))
- for _, r := range r {
+func (c compositeRule) String() string {
+ s := make([]string, 0, len(c))
+ for _, r := range c {
s = append(s, r.String())
}
@@ -173,9 +193,12 @@
ctx.BottomUp("visibilityRuleChecker", visibilityRuleChecker).Parallel()
}
+// Registers the function that gathers the visibility rules for each module.
+//
// Visibility is not dependent on arch so this must be registered before the arch phase to avoid
// having to process multiple variants for each module. This goes after defaults expansion to gather
-// the complete visibility lists from flat lists.
+// the complete visibility lists from flat lists and after the package info is gathered to ensure
+// that default_visibility is available.
func registerVisibilityRuleGatherer(ctx RegisterMutatorsContext) {
ctx.BottomUp("visibilityRuleGatherer", visibilityRuleGatherer).Parallel()
}
@@ -193,33 +216,36 @@
for _, props := range d.properties() {
if cp, ok := props.(*commonProperties); ok {
if visibility := cp.Visibility; visibility != nil {
- checkRules(ctx, qualified.pkg, visibility)
+ checkRules(ctx, qualified.pkg, "visibility", visibility)
}
}
}
} else if m, ok := ctx.Module().(Module); ok {
- if visibility := m.base().commonProperties.Visibility; visibility != nil {
- checkRules(ctx, qualified.pkg, visibility)
+ visibilityProperties := m.visibilityProperties()
+ for _, p := range visibilityProperties {
+ if visibility := p.getStrings(); visibility != nil {
+ checkRules(ctx, qualified.pkg, p.getName(), visibility)
+ }
}
}
}
-func checkRules(ctx BottomUpMutatorContext, currentPkg string, visibility []string) {
+func checkRules(ctx BaseModuleContext, currentPkg, property string, visibility []string) {
ruleCount := len(visibility)
if ruleCount == 0 {
// This prohibits an empty list as its meaning is unclear, e.g. it could mean no visibility and
// it could mean public visibility. Requiring at least one rule makes the owner's intent
// clearer.
- ctx.PropertyErrorf("visibility", "must contain at least one visibility rule")
+ ctx.PropertyErrorf(property, "must contain at least one visibility rule")
return
}
for _, v := range visibility {
- ok, pkg, name := splitRule(ctx, v, currentPkg)
+ ok, pkg, name := splitRule(v, currentPkg)
if !ok {
// Visibility rule is invalid so ignore it. Keep going rather than aborting straight away to
// ensure all the rules on this module are checked.
- ctx.PropertyErrorf("visibility",
+ ctx.PropertyErrorf(property,
"invalid visibility pattern %q must match"+
" //<package>:<module>, //<package> or :<module>",
v)
@@ -230,14 +256,14 @@
switch name {
case "private", "public":
case "legacy_public":
- ctx.PropertyErrorf("visibility", "//visibility:legacy_public must not be used")
+ ctx.PropertyErrorf(property, "//visibility:legacy_public must not be used")
continue
default:
- ctx.PropertyErrorf("visibility", "unrecognized visibility rule %q", v)
+ ctx.PropertyErrorf(property, "unrecognized visibility rule %q", v)
continue
}
if ruleCount != 1 {
- ctx.PropertyErrorf("visibility", "cannot mix %q with any other visibility rules", v)
+ ctx.PropertyErrorf(property, "cannot mix %q with any other visibility rules", v)
continue
}
}
@@ -246,7 +272,7 @@
// restrictions on the rules.
if !isAncestor("vendor", currentPkg) {
if !isAllowedFromOutsideVendor(pkg, name) {
- ctx.PropertyErrorf("visibility",
+ ctx.PropertyErrorf(property,
"%q is not allowed. Packages outside //vendor cannot make themselves visible to specific"+
" targets within //vendor, they can only use //vendor:__subpackages__.", v)
continue
@@ -265,23 +291,27 @@
return
}
- qualified := createQualifiedModuleName(ctx)
+ qualifiedModuleId := m.qualifiedModuleId(ctx)
+ currentPkg := qualifiedModuleId.pkg
- visibility := m.base().commonProperties.Visibility
- if visibility != nil {
- rule := parseRules(ctx, qualified.pkg, visibility)
- if rule != nil {
- moduleToVisibilityRuleMap(ctx).Store(qualified, rule)
+ // Parse all the properties into rules and store them.
+ visibilityProperties := m.visibilityProperties()
+ for _, p := range visibilityProperties {
+ if visibility := p.getStrings(); visibility != nil {
+ rule := parseRules(ctx, currentPkg, visibility)
+ if rule != nil {
+ moduleToVisibilityRuleMap(ctx).Store(qualifiedModuleId, rule)
+ }
}
}
}
-func parseRules(ctx BottomUpMutatorContext, currentPkg string, visibility []string) compositeRule {
+func parseRules(ctx BaseModuleContext, currentPkg string, visibility []string) compositeRule {
rules := make(compositeRule, 0, len(visibility))
hasPrivateRule := false
hasNonPrivateRule := false
for _, v := range visibility {
- ok, pkg, name := splitRule(ctx, v, currentPkg)
+ ok, pkg, name := splitRule(v, currentPkg)
if !ok {
continue
}
@@ -336,7 +366,7 @@
return !isAncestor("vendor", pkg)
}
-func splitRule(ctx BaseModuleContext, ruleExpression string, currentPkg string) (bool, string, string) {
+func splitRule(ruleExpression string, currentPkg string) (bool, string, string) {
// Make sure that the rule is of the correct format.
matches := visibilityRuleRegexp.FindStringSubmatch(ruleExpression)
if ruleExpression == "" || matches == nil {
@@ -378,11 +408,15 @@
return
}
- rule, ok := moduleToVisibilityRule.Load(depQualified)
+ value, ok := moduleToVisibilityRule.Load(depQualified)
+ var rule compositeRule
if ok {
- if !rule.(compositeRule).matches(qualified) {
- ctx.ModuleErrorf("depends on %s which is not visible to this module", depQualified)
- }
+ rule = value.(compositeRule)
+ } else {
+ rule = packageDefaultVisibility(ctx, depQualified)
+ }
+ if rule != nil && !rule.matches(qualified) {
+ ctx.ModuleErrorf("depends on %s which is not visible to this module", depQualified)
}
})
}
@@ -393,3 +427,20 @@
qualified := qualifiedModuleName{dir, moduleName}
return qualified
}
+
+func packageDefaultVisibility(ctx BaseModuleContext, moduleId qualifiedModuleName) compositeRule {
+ moduleToVisibilityRule := moduleToVisibilityRuleMap(ctx)
+ packageQualifiedId := moduleId.getContainingPackageId()
+ for {
+ value, ok := moduleToVisibilityRule.Load(packageQualifiedId)
+ if ok {
+ return value.(compositeRule)
+ }
+
+ if packageQualifiedId.isRootPackage() {
+ return nil
+ }
+
+ packageQualifiedId = packageQualifiedId.getContainingPackageId()
+ }
+}
diff --git a/android/visibility_test.go b/android/visibility_test.go
index 1a51495..af6acf4 100644
--- a/android/visibility_test.go
+++ b/android/visibility_test.go
@@ -658,6 +658,167 @@
` visible to this module`,
},
},
+ // Package default_visibility tests
+ {
+ name: "package default_visibility property is checked",
+ fs: map[string][]byte{
+ "top/Blueprints": []byte(`
+ package {
+ default_visibility: ["//visibility:invalid"],
+ }`),
+ },
+ expectedErrors: []string{`default_visibility: unrecognized visibility rule "//visibility:invalid"`},
+ },
+ {
+ // This test relies on the default visibility being legacy_public.
+ name: "package default_visibility property used when no visibility specified",
+ fs: map[string][]byte{
+ "top/Blueprints": []byte(`
+ package {
+ default_visibility: ["//visibility:private"],
+ }
+
+ mock_library {
+ name: "libexample",
+ }`),
+ "outsider/Blueprints": []byte(`
+ mock_library {
+ name: "liboutsider",
+ deps: ["libexample"],
+ }`),
+ },
+ expectedErrors: []string{
+ `module "liboutsider" variant "android_common": depends on //top:libexample which is not` +
+ ` visible to this module`,
+ },
+ },
+ {
+ name: "package default_visibility public does not override visibility private",
+ fs: map[string][]byte{
+ "top/Blueprints": []byte(`
+ package {
+ default_visibility: ["//visibility:public"],
+ }
+
+ mock_library {
+ name: "libexample",
+ visibility: ["//visibility:private"],
+ }`),
+ "outsider/Blueprints": []byte(`
+ mock_library {
+ name: "liboutsider",
+ deps: ["libexample"],
+ }`),
+ },
+ expectedErrors: []string{
+ `module "liboutsider" variant "android_common": depends on //top:libexample which is not` +
+ ` visible to this module`,
+ },
+ },
+ {
+ name: "package default_visibility private does not override visibility public",
+ fs: map[string][]byte{
+ "top/Blueprints": []byte(`
+ package {
+ default_visibility: ["//visibility:private"],
+ }
+
+ mock_library {
+ name: "libexample",
+ visibility: ["//visibility:public"],
+ }`),
+ "outsider/Blueprints": []byte(`
+ mock_library {
+ name: "liboutsider",
+ deps: ["libexample"],
+ }`),
+ },
+ },
+ {
+ name: "package default_visibility :__subpackages__",
+ fs: map[string][]byte{
+ "top/Blueprints": []byte(`
+ package {
+ default_visibility: [":__subpackages__"],
+ }
+
+ mock_library {
+ name: "libexample",
+ }`),
+ "top/nested/Blueprints": []byte(`
+ mock_library {
+ name: "libnested",
+ deps: ["libexample"],
+ }`),
+ "outsider/Blueprints": []byte(`
+ mock_library {
+ name: "liboutsider",
+ deps: ["libexample"],
+ }`),
+ },
+ expectedErrors: []string{
+ `module "liboutsider" variant "android_common": depends on //top:libexample which is not` +
+ ` visible to this module`,
+ },
+ },
+ {
+ name: "package default_visibility inherited to subpackages",
+ fs: map[string][]byte{
+ "top/Blueprints": []byte(`
+ package {
+ default_visibility: ["//outsider"],
+ }
+
+ mock_library {
+ name: "libexample",
+ visibility: [":__subpackages__"],
+ }`),
+ "top/nested/Blueprints": []byte(`
+ mock_library {
+ name: "libnested",
+ deps: ["libexample"],
+ }`),
+ "outsider/Blueprints": []byte(`
+ mock_library {
+ name: "liboutsider",
+ deps: ["libexample", "libnested"],
+ }`),
+ },
+ expectedErrors: []string{
+ `module "liboutsider" variant "android_common": depends on //top:libexample which is not` +
+ ` visible to this module`,
+ },
+ },
+ {
+ name: "package default_visibility inherited to subpackages",
+ fs: map[string][]byte{
+ "top/Blueprints": []byte(`
+ package {
+ default_visibility: ["//visibility:private"],
+ }`),
+ "top/nested/Blueprints": []byte(`
+ package {
+ default_visibility: ["//outsider"],
+ }
+
+ mock_library {
+ name: "libnested",
+ }`),
+ "top/other/Blueprints": []byte(`
+ mock_library {
+ name: "libother",
+ }`),
+ "outsider/Blueprints": []byte(`
+ mock_library {
+ name: "liboutsider",
+ deps: ["libother", "libnested"],
+ }`),
+ },
+ expectedErrors: []string{
+ `module "liboutsider" variant "android_common": depends on //top/other:libother which is` +
+ ` not visible to this module`,
+ },
+ },
}
func TestVisibility(t *testing.T) {
@@ -692,8 +853,10 @@
config := TestArchConfig(buildDir, nil)
ctx := NewTestArchContext()
+ ctx.RegisterModuleType("package", ModuleFactoryAdaptor(PackageFactory))
ctx.RegisterModuleType("mock_library", ModuleFactoryAdaptor(newMockLibraryModule))
ctx.RegisterModuleType("mock_defaults", ModuleFactoryAdaptor(defaultsFactory))
+ ctx.PreArchMutators(registerPackageRenamer)
ctx.PreArchMutators(registerVisibilityRuleChecker)
ctx.PreArchMutators(RegisterDefaultsPreArchMutators)
ctx.PreArchMutators(registerVisibilityRuleGatherer)
diff --git a/apex/apex.go b/apex/apex.go
index 84e5497..a77e295 100644
--- a/apex/apex.go
+++ b/apex/apex.go
@@ -90,12 +90,6 @@
CommandDeps: []string{"${zip2zip}"},
Description: "app bundle",
}, "abi")
-
- apexMergeNoticeRule = pctx.StaticRule("apexMergeNoticeRule", blueprint.RuleParams{
- Command: `${mergenotice} --output $out $inputs`,
- CommandDeps: []string{"${mergenotice}"},
- Description: "merge notice files into $out",
- }, "inputs")
)
var imageApexSuffix = ".apex"
@@ -144,8 +138,6 @@
pctx.HostBinToolVariable("zip2zip", "zip2zip")
pctx.HostBinToolVariable("zipalign", "zipalign")
- pctx.SourcePathVariable("mergenotice", "build/soong/scripts/mergenotice.py")
-
android.RegisterModuleType("apex", apexBundleFactory)
android.RegisterModuleType("apex_test", testApexBundleFactory)
android.RegisterModuleType("apex_defaults", defaultsFactory)
@@ -153,7 +145,7 @@
android.PostDepsMutators(func(ctx android.RegisterMutatorsContext) {
ctx.TopDown("apex_deps", apexDepsMutator)
- ctx.BottomUp("apex", apexMutator)
+ ctx.BottomUp("apex", apexMutator).Parallel()
})
}
@@ -402,8 +394,6 @@
container_certificate_file android.Path
container_private_key_file android.Path
- mergedNoticeFile android.WritablePath
-
// list of files to be included in this apex
filesInfo []apexFile
@@ -836,8 +826,6 @@
a.installDir = android.PathForModuleInstall(ctx, "apex")
a.filesInfo = filesInfo
- a.buildNoticeFile(ctx)
-
if a.apexTypes.zip() {
a.buildUnflattenedApex(ctx, zipApex)
}
@@ -851,35 +839,27 @@
}
}
-func (a *apexBundle) buildNoticeFile(ctx android.ModuleContext) {
+func (a *apexBundle) buildNoticeFile(ctx android.ModuleContext, apexFileName string) android.OptionalPath {
noticeFiles := []android.Path{}
- noticeFilesString := []string{}
for _, f := range a.filesInfo {
if f.module != nil {
notice := f.module.NoticeFile()
if notice.Valid() {
noticeFiles = append(noticeFiles, notice.Path())
- noticeFilesString = append(noticeFilesString, notice.Path().String())
}
}
}
// append the notice file specified in the apex module itself
if a.NoticeFile().Valid() {
noticeFiles = append(noticeFiles, a.NoticeFile().Path())
- noticeFilesString = append(noticeFilesString, a.NoticeFile().Path().String())
}
- if len(noticeFiles) > 0 {
- a.mergedNoticeFile = android.PathForModuleOut(ctx, "NOTICE")
- ctx.Build(pctx, android.BuildParams{
- Rule: apexMergeNoticeRule,
- Inputs: noticeFiles,
- Output: a.mergedNoticeFile,
- Args: map[string]string{
- "inputs": strings.Join(noticeFilesString, " "),
- },
- })
+ if len(noticeFiles) == 0 {
+ return android.OptionalPath{}
}
+
+ return android.OptionalPathForPath(
+ android.BuildNoticeOutput(ctx, a.installDir, apexFileName, android.FirstUniquePaths(noticeFiles)))
}
func (a *apexBundle) buildUnflattenedApex(ctx android.ModuleContext, apexType apexPackaging) {
@@ -1004,6 +984,13 @@
}
optFlags = append(optFlags, "--target_sdk_version "+targetSdkVersion)
+ noticeFile := a.buildNoticeFile(ctx, ctx.ModuleName()+suffix)
+ if noticeFile.Valid() {
+ // If there's a NOTICE file, embed it as an asset file in the APEX.
+ implicitInputs = append(implicitInputs, noticeFile.Path())
+ optFlags = append(optFlags, "--assets_dir "+filepath.Dir(noticeFile.String()))
+ }
+
ctx.Build(pctx, android.BuildParams{
Rule: apexRule,
Implicits: implicitInputs,
@@ -1250,9 +1237,6 @@
fmt.Fprintln(w, "LOCAL_MODULE_PATH :=", filepath.Join("$(OUT_DIR)", a.installDir.RelPathString()))
fmt.Fprintln(w, "LOCAL_MODULE_STEM :=", name+apexType.suffix())
fmt.Fprintln(w, "LOCAL_UNINSTALLABLE_MODULE :=", !a.installable())
- if a.installable() && a.mergedNoticeFile != nil {
- fmt.Fprintln(w, "LOCAL_NOTICE_FILE :=", a.mergedNoticeFile.String())
- }
if len(moduleNames) > 0 {
fmt.Fprintln(w, "LOCAL_REQUIRED_MODULES +=", strings.Join(moduleNames, " "))
}
diff --git a/apex/apex_test.go b/apex/apex_test.go
index f71abd7..2e44db7 100644
--- a/apex/apex_test.go
+++ b/apex/apex_test.go
@@ -27,9 +27,15 @@
"android/soong/java"
)
+var buildDir string
+
func testApex(t *testing.T, bp string) *android.TestContext {
- config, buildDir := setup(t)
- defer teardown(buildDir)
+ config := android.TestArchConfig(buildDir, nil)
+ config.TestProductVariables.DeviceVndkVersion = proptools.StringPtr("current")
+ config.TestProductVariables.DefaultAppCertificate = proptools.StringPtr("vendor/foo/devkeys/test")
+ config.TestProductVariables.CertificateOverrides = []string{"myapex_keytest:myapex.certificate.override"}
+ config.TestProductVariables.Platform_sdk_codename = proptools.StringPtr("Q")
+ config.TestProductVariables.Platform_sdk_final = proptools.BoolPtr(false)
ctx := android.NewTestArchContext()
ctx.RegisterModuleType("apex", android.ModuleFactoryAdaptor(apexBundleFactory))
@@ -188,22 +194,15 @@
return ctx
}
-func setup(t *testing.T) (config android.Config, buildDir string) {
- buildDir, err := ioutil.TempDir("", "soong_apex_test")
+func setUp() {
+ var err error
+ buildDir, err = ioutil.TempDir("", "soong_apex_test")
if err != nil {
- t.Fatal(err)
+ panic(err)
}
-
- config = android.TestArchConfig(buildDir, nil)
- config.TestProductVariables.DeviceVndkVersion = proptools.StringPtr("current")
- config.TestProductVariables.DefaultAppCertificate = proptools.StringPtr("vendor/foo/devkeys/test")
- config.TestProductVariables.CertificateOverrides = []string{"myapex_keytest:myapex.certificate.override"}
- config.TestProductVariables.Platform_sdk_codename = proptools.StringPtr("Q")
- config.TestProductVariables.Platform_sdk_final = proptools.BoolPtr(false)
- return
}
-func teardown(buildDir string) {
+func tearDown() {
os.RemoveAll(buildDir)
}
@@ -310,6 +309,8 @@
optFlags := apexRule.Args["opt_flags"]
ensureContains(t, optFlags, "--pubkey vendor/foo/devkeys/testkey.avbpubkey")
+ // Ensure that the NOTICE output is being packaged as an asset.
+ ensureContains(t, optFlags, "--assets_dir "+buildDir+"/.intermediates/myapex/android_common_myapex/NOTICE")
copyCmds := apexRule.Args["copy_commands"]
@@ -347,10 +348,10 @@
t.Errorf("Could not find all expected symlinks! foo: %t, foo_link_64: %t. Command was %s", found_foo, found_foo_link_64, copyCmds)
}
- apexMergeNoticeRule := ctx.ModuleForTests("myapex", "android_common_myapex").Rule("apexMergeNoticeRule")
- noticeInputs := strings.Split(apexMergeNoticeRule.Args["inputs"], " ")
- if len(noticeInputs) != 3 {
- t.Errorf("number of input notice files: expected = 3, actual = %d", len(noticeInputs))
+ mergeNoticesRule := ctx.ModuleForTests("myapex", "android_common_myapex").Rule("mergeNoticesRule")
+ noticeInputs := mergeNoticesRule.Inputs.Strings()
+ if len(noticeInputs) != 2 {
+ t.Errorf("number of input notice files: expected = 2, actual = %q", len(noticeInputs))
}
ensureListContains(t, noticeInputs, "NOTICE")
ensureListContains(t, noticeInputs, "custom_notice")
@@ -1283,3 +1284,14 @@
t.Errorf("installFilename invalid. expected: %q, actual: %q", expected, p.installFilename)
}
}
+
+func TestMain(m *testing.M) {
+ run := func() int {
+ setUp()
+ defer tearDown()
+
+ return m.Run()
+ }
+
+ os.Exit(run())
+}
diff --git a/cc/builder.go b/cc/builder.go
index 1e12361..ee40736 100644
--- a/cc/builder.go
+++ b/cc/builder.go
@@ -416,7 +416,7 @@
ccCmd = "clang"
moduleCflags = cflags
moduleToolingCflags = toolingCflags
- case ".cpp", ".cc", ".mm":
+ case ".cpp", ".cc", ".cxx", ".mm":
ccCmd = "clang++"
moduleCflags = cppflags
moduleToolingCflags = toolingCppflags
diff --git a/cc/cc.go b/cc/cc.go
index 53ec899..6215067 100644
--- a/cc/cc.go
+++ b/cc/cc.go
@@ -65,7 +65,7 @@
ctx.TopDown("tsan_deps", sanitizerDepsMutator(tsan))
ctx.BottomUp("tsan", sanitizerMutator(tsan)).Parallel()
- ctx.TopDown("sanitize_runtime_deps", sanitizerRuntimeDepsMutator)
+ ctx.TopDown("sanitize_runtime_deps", sanitizerRuntimeDepsMutator).Parallel()
ctx.BottomUp("sanitize_runtime", sanitizerRuntimeMutator).Parallel()
ctx.BottomUp("coverage", coverageMutator).Parallel()
@@ -778,6 +778,10 @@
// APEX variants do not need ABI dumps.
return false
}
+ if ctx.isStubs() {
+ // Stubs do not need ABI dumps.
+ return false
+ }
if ctx.isNdk() {
return true
}
diff --git a/cc/compdb.go b/cc/compdb.go
index 1102651..ecc67b8 100644
--- a/cc/compdb.go
+++ b/cc/compdb.go
@@ -141,7 +141,7 @@
isAsm = false
isCpp = false
clangPath = ccPath
- case ".cpp", ".cc", ".mm":
+ case ".cpp", ".cc", ".cxx", ".mm":
isAsm = false
isCpp = true
clangPath = cxxPath
diff --git a/cc/config/x86_64_device.go b/cc/config/x86_64_device.go
index 0f0420f..d9c96df 100644
--- a/cc/config/x86_64_device.go
+++ b/cc/config/x86_64_device.go
@@ -67,10 +67,16 @@
"sse4": []string{"-msse4"},
"sse4_1": []string{"-msse4.1"},
"sse4_2": []string{"-msse4.2"},
+
+ // Not all cases there is performance gain by enabling -mavx -mavx2
+ // flags so these flags are not enabled by default.
+ // if there is performance gain in individual library components,
+ // the compiler flags can be set in corresponding bp files.
+ // "avx": []string{"-mavx"},
+ // "avx2": []string{"-mavx2"},
+ // "avx512": []string{"-mavx512"}
+
"popcnt": []string{"-mpopcnt"},
- "avx": []string{"-mavx"},
- "avx2": []string{"-mavx2"},
- "avx512": []string{"-mavx512"},
"aes_ni": []string{"-maes"},
}
)
diff --git a/cc/config/x86_device.go b/cc/config/x86_device.go
index 500014e..6504758 100644
--- a/cc/config/x86_device.go
+++ b/cc/config/x86_device.go
@@ -86,8 +86,15 @@
"sse4": []string{"-msse4"},
"sse4_1": []string{"-msse4.1"},
"sse4_2": []string{"-msse4.2"},
- "avx": []string{"-mavx"},
- "avx2": []string{"-mavx2"},
+
+ // Not all cases there is performance gain by enabling -mavx -mavx2
+ // flags so these flags are not enabled by default.
+ // if there is performance gain in individual library components,
+ // the compiler flags can be set in corresponding bp files.
+ // "avx": []string{"-mavx"},
+ // "avx2": []string{"-mavx2"},
+ // "avx512": []string{"-mavx512"}
+
"aes_ni": []string{"-maes"},
}
)
diff --git a/cc/gen.go b/cc/gen.go
index 1d30dab..7516b28 100644
--- a/cc/gen.go
+++ b/cc/gen.go
@@ -25,6 +25,7 @@
func init() {
pctx.SourcePathVariable("lexCmd", "prebuilts/build-tools/${config.HostPrebuiltTag}/bin/flex")
+ pctx.SourcePathVariable("m4Cmd", "prebuilts/build-tools/${config.HostPrebuiltTag}/bin/m4")
pctx.HostBinToolVariable("aidlCmd", "aidl-cpp")
pctx.HostBinToolVariable("syspropCmd", "sysprop_cpp")
@@ -33,8 +34,8 @@
var (
lex = pctx.AndroidStaticRule("lex",
blueprint.RuleParams{
- Command: "$lexCmd -o$out $in",
- CommandDeps: []string{"$lexCmd"},
+ Command: "M4=$m4Cmd $lexCmd -o$out $in",
+ CommandDeps: []string{"$lexCmd", "$m4Cmd"},
})
sysprop = pctx.AndroidStaticRule("sysprop",
@@ -97,6 +98,7 @@
}
cmd.Text("BISON_PKGDATADIR=prebuilts/build-tools/common/bison").
+ FlagWithInput("M4=", ctx.Config().PrebuiltBuildTool(ctx, "m4")).
Tool(ctx.Config().PrebuiltBuildTool(ctx, "bison")).
Flag("-d").
Flags(flags).
diff --git a/cc/library.go b/cc/library.go
index f98cd36..5fbb36e 100644
--- a/cc/library.go
+++ b/cc/library.go
@@ -810,7 +810,7 @@
}
func (library *libraryDecorator) linkSAbiDumpFiles(ctx ModuleContext, objs Objects, fileName string, soFile android.Path) {
- if len(objs.sAbiDumpFiles) > 0 && library.shouldCreateVndkSourceAbiDump(ctx) {
+ if library.shouldCreateVndkSourceAbiDump(ctx) {
vndkVersion := ctx.DeviceConfig().PlatformVndkVersion()
if ver := ctx.DeviceConfig().VndkVersion(); ver != "" && ver != "current" {
vndkVersion = ver
diff --git a/cc/makevars.go b/cc/makevars.go
index a71f479..78a32c8 100644
--- a/cc/makevars.go
+++ b/cc/makevars.go
@@ -161,6 +161,8 @@
ctx.Strict("AIDL_CPP", "${aidlCmd}")
+ ctx.Strict("M4", "${m4Cmd}")
+
ctx.Strict("RS_GLOBAL_INCLUDES", "${config.RsGlobalIncludes}")
ctx.Strict("SOONG_STRIP_PATH", "${stripPath}")
diff --git a/cc/sanitize.go b/cc/sanitize.go
index 0af0659..0eb9a74 100644
--- a/cc/sanitize.go
+++ b/cc/sanitize.go
@@ -468,6 +468,10 @@
// TODO(b/131771163): LTO and Fuzzer support is mutually incompatible.
_, flags.LdFlags = removeFromList("-flto", flags.LdFlags)
flags.LdFlags = append(flags.LdFlags, "-fno-lto")
+
+ // TODO(b/133876586): Experimental PM breaks sanitizer coverage.
+ _, flags.CFlags = removeFromList("-fexperimental-new-pass-manager", flags.CFlags)
+ flags.CFlags = append(flags.CFlags, "-fno-experimental-new-pass-manager")
}
if Bool(sanitize.Properties.Sanitize.Cfi) {
@@ -701,8 +705,8 @@
if !isSanitizableDependencyTag(mctx.OtherModuleDependencyTag(child)) {
return false
}
- if d, ok := child.(*Module); ok && d.static() && d.sanitize != nil {
+ if d, ok := child.(*Module); ok && d.static() && d.sanitize != nil {
if enableMinimalRuntime(d.sanitize) {
// If a static dependency is built with the minimal runtime,
// make sure we include the ubsan minimal runtime.
@@ -713,8 +717,17 @@
// make sure we include the ubsan runtime.
c.sanitize.Properties.UbsanRuntimeDep = true
}
+
+ if c.sanitize.Properties.MinimalRuntimeDep &&
+ c.sanitize.Properties.UbsanRuntimeDep {
+ // both flags that this mutator might set are true, so don't bother recursing
+ return false
+ }
+
+ return true
+ } else {
+ return false
}
- return true
})
}
}
diff --git a/cmd/soong_ui/main.go b/cmd/soong_ui/main.go
index b909779..4d59a39 100644
--- a/cmd/soong_ui/main.go
+++ b/cmd/soong_ui/main.go
@@ -161,6 +161,8 @@
trace.SetOutput(filepath.Join(logsDir, "build.trace"))
stat.AddOutput(status.NewVerboseLog(log, filepath.Join(logsDir, "verbose.log")))
stat.AddOutput(status.NewErrorLog(log, filepath.Join(logsDir, "error.log")))
+ stat.AddOutput(status.NewProtoErrorLog(log, filepath.Join(logsDir, "build_error")))
+ stat.AddOutput(status.NewCriticalPath(log))
defer met.Dump(filepath.Join(logsDir, "soong_metrics"))
@@ -337,7 +339,7 @@
}{{
name: "all-modules",
description: "Build action: build from the top of the source tree.",
- action: build.BUILD_MODULES_IN_A_DIRECTORY,
+ action: build.BUILD_MODULES,
buildDependencies: true,
}, {
name: "modules-in-a-dir-no-deps",
diff --git a/dexpreopt/config.go b/dexpreopt/config.go
index a2f1af4..3e32958 100644
--- a/dexpreopt/config.go
+++ b/dexpreopt/config.go
@@ -79,13 +79,11 @@
InstructionSetFeatures map[android.ArchType]string // instruction set for each architecture
// Only used for boot image
- DirtyImageObjects android.OptionalPath // path to a dirty-image-objects file
- PreloadedClasses android.OptionalPath // path to a preloaded-classes file
- BootImageProfiles android.Paths // path to a boot-image-profile.txt file
- UseProfileForBootImage bool // whether a profile should be used to compile the boot image
- BootFlags string // extra flags to pass to dex2oat for the boot image
- Dex2oatImageXmx string // max heap size for dex2oat for the boot image
- Dex2oatImageXms string // initial heap size for dex2oat for the boot image
+ DirtyImageObjects android.OptionalPath // path to a dirty-image-objects file
+ BootImageProfiles android.Paths // path to a boot-image-profile.txt file
+ BootFlags string // extra flags to pass to dex2oat for the boot image
+ Dex2oatImageXmx string // max heap size for dex2oat for the boot image
+ Dex2oatImageXms string // initial heap size for dex2oat for the boot image
Tools Tools // paths to tools possibly used by the generated commands
}
@@ -183,7 +181,6 @@
// Copies of entries in GlobalConfig that are not constructable without extra parameters. They will be
// used to construct the real value manually below.
DirtyImageObjects string
- PreloadedClasses string
BootImageProfiles []string
Tools struct {
@@ -206,7 +203,6 @@
// Construct paths that require a PathContext.
config.GlobalConfig.DirtyImageObjects = android.OptionalPathForPath(constructPath(ctx, config.DirtyImageObjects))
- config.GlobalConfig.PreloadedClasses = android.OptionalPathForPath(constructPath(ctx, config.PreloadedClasses))
config.GlobalConfig.BootImageProfiles = constructPaths(ctx, config.BootImageProfiles)
config.GlobalConfig.Tools.Profman = constructPath(ctx, config.Tools.Profman)
@@ -321,9 +317,7 @@
CpuVariant: nil,
InstructionSetFeatures: nil,
DirtyImageObjects: android.OptionalPath{},
- PreloadedClasses: android.OptionalPath{},
BootImageProfiles: nil,
- UseProfileForBootImage: false,
BootFlags: "",
Dex2oatImageXmx: "",
Dex2oatImageXms: "",
diff --git a/finder/finder_test.go b/finder/finder_test.go
index 29711fc..f6d0aa9 100644
--- a/finder/finder_test.go
+++ b/finder/finder_test.go
@@ -891,8 +891,8 @@
IncludeFiles: []string{"findme.txt"},
},
)
- foundPaths := finder.FindNamedAt("/tmp", "findme.txt")
filesystem.Clock.Tick()
+ foundPaths := finder.FindNamedAt("/tmp", "findme.txt")
finder.Shutdown()
// check the response of the first finder
assertSameResponse(t, foundPaths, []string{"/tmp/a/findme.txt"})
@@ -1522,8 +1522,8 @@
IncludeFiles: []string{"hi.txt"},
},
)
- foundPaths := finder.FindAll()
filesystem.Clock.Tick()
+ foundPaths := finder.FindAll()
finder.Shutdown()
// check results
assertSameResponse(t, foundPaths, []string{"/tmp/a/hi.txt"})
@@ -1583,8 +1583,8 @@
IncludeFiles: []string{"hi.txt"},
},
)
- foundPaths := finder.FindAll()
filesystem.Clock.Tick()
+ foundPaths := finder.FindAll()
finder.Shutdown()
allPaths := []string{"/tmp/hi.txt", "/tmp/a/hi.txt", "/tmp/a/a/hi.txt", "/tmp/b/hi.txt"}
// check results
@@ -1629,8 +1629,8 @@
IncludeFiles: []string{"hi.txt"},
},
)
- foundPaths := finder.FindAll()
filesystem.Clock.Tick()
+ foundPaths := finder.FindAll()
finder.Shutdown()
// check results
assertSameResponse(t, foundPaths, []string{"/tmp/hi.txt"})
@@ -1650,8 +1650,8 @@
IncludeFiles: []string{"hi.txt"},
},
)
- foundPaths := finder.FindAll()
filesystem.Clock.Tick()
+ foundPaths := finder.FindAll()
finder.Shutdown()
// check results
assertSameResponse(t, foundPaths, []string{"/tmp/a/hi.txt"})
diff --git a/java/aapt2.go b/java/aapt2.go
index a815160..f0eb99c 100644
--- a/java/aapt2.go
+++ b/java/aapt2.go
@@ -55,12 +55,14 @@
var aapt2CompileRule = pctx.AndroidStaticRule("aapt2Compile",
blueprint.RuleParams{
- Command: `${config.Aapt2Cmd} compile -o $outDir $cFlags --legacy $in`,
+ Command: `${config.Aapt2Cmd} compile -o $outDir $cFlags $in`,
CommandDeps: []string{"${config.Aapt2Cmd}"},
},
"outDir", "cFlags")
-func aapt2Compile(ctx android.ModuleContext, dir android.Path, paths android.Paths) android.WritablePaths {
+func aapt2Compile(ctx android.ModuleContext, dir android.Path, paths android.Paths,
+ flags []string) android.WritablePaths {
+
shards := shardPaths(paths, AAPT2_SHARD_SIZE)
ret := make(android.WritablePaths, 0, len(paths))
@@ -81,9 +83,7 @@
Outputs: outPaths,
Args: map[string]string{
"outDir": android.PathForModuleOut(ctx, "aapt2", dir.String()).String(),
- // Always set --pseudo-localize, it will be stripped out later for release
- // builds that don't want it.
- "cFlags": "--pseudo-localize",
+ "cFlags": strings.Join(flags, " "),
},
})
}
@@ -97,14 +97,16 @@
var aapt2CompileZipRule = pctx.AndroidStaticRule("aapt2CompileZip",
blueprint.RuleParams{
Command: `${config.ZipSyncCmd} -d $resZipDir $zipSyncFlags $in && ` +
- `${config.Aapt2Cmd} compile -o $out $cFlags --legacy --dir $resZipDir`,
+ `${config.Aapt2Cmd} compile -o $out $cFlags --dir $resZipDir`,
CommandDeps: []string{
"${config.Aapt2Cmd}",
"${config.ZipSyncCmd}",
},
}, "cFlags", "resZipDir", "zipSyncFlags")
-func aapt2CompileZip(ctx android.ModuleContext, flata android.WritablePath, zip android.Path, zipPrefix string) {
+func aapt2CompileZip(ctx android.ModuleContext, flata android.WritablePath, zip android.Path, zipPrefix string,
+ flags []string) {
+
if zipPrefix != "" {
zipPrefix = "--zip-prefix " + zipPrefix
}
@@ -114,9 +116,7 @@
Input: zip,
Output: flata,
Args: map[string]string{
- // Always set --pseudo-localize, it will be stripped out later for release
- // builds that don't want it.
- "cFlags": "--pseudo-localize",
+ "cFlags": strings.Join(flags, " "),
"resZipDir": android.PathForModuleOut(ctx, "aapt2", "reszip", flata.Base()).String(),
"zipSyncFlags": zipPrefix,
},
diff --git a/java/aar.go b/java/aar.go
index a2c203f..ce3d126 100644
--- a/java/aar.go
+++ b/java/aar.go
@@ -17,6 +17,7 @@
import (
"android/soong/android"
"fmt"
+ "path/filepath"
"strings"
"github.com/google/blueprint"
@@ -80,6 +81,7 @@
rTxt android.Path
extraAaptPackagesFile android.Path
mergedManifestFile android.Path
+ noticeFile android.OptionalPath
isLibrary bool
useEmbeddedNativeLibs bool
useEmbeddedDex bool
@@ -111,8 +113,9 @@
return a.transitiveManifestPaths
}
-func (a *aapt) aapt2Flags(ctx android.ModuleContext, sdkContext sdkContext, manifestPath android.Path) (flags []string,
- deps android.Paths, resDirs, overlayDirs []globbedResourceDir, rroDirs []rroDir, resZips android.Paths) {
+func (a *aapt) aapt2Flags(ctx android.ModuleContext, sdkContext sdkContext,
+ manifestPath android.Path) (compileFlags, linkFlags []string, linkDeps android.Paths,
+ resDirs, overlayDirs []globbedResourceDir, rroDirs []rroDir, resZips android.Paths) {
hasVersionCode := false
hasVersionName := false
@@ -124,8 +127,6 @@
}
}
- var linkFlags []string
-
// Flags specified in Android.bp
linkFlags = append(linkFlags, a.aaptProperties.Aaptflags...)
@@ -136,8 +137,6 @@
resourceDirs := android.PathsWithOptionalDefaultForModuleSrc(ctx, a.aaptProperties.Resource_dirs, "res")
resourceZips := android.PathsForModuleSrc(ctx, a.aaptProperties.Resource_zips)
- var linkDeps android.Paths
-
// Glob directories into lists of paths
for _, dir := range resourceDirs {
resDirs = append(resDirs, globbedResourceDir{
@@ -154,10 +153,16 @@
assetFiles = append(assetFiles, androidResourceGlob(ctx, dir)...)
}
+ assetDirStrings := assetDirs.Strings()
+ if a.noticeFile.Valid() {
+ assetDirStrings = append(assetDirStrings, filepath.Dir(a.noticeFile.Path().String()))
+ assetFiles = append(assetFiles, a.noticeFile.Path())
+ }
+
linkFlags = append(linkFlags, "--manifest "+manifestPath.String())
linkDeps = append(linkDeps, manifestPath)
- linkFlags = append(linkFlags, android.JoinWithPrefix(assetDirs.Strings(), "-A "))
+ linkFlags = append(linkFlags, android.JoinWithPrefix(assetDirStrings, "-A "))
linkDeps = append(linkDeps, assetFiles...)
// SDK version flags
@@ -185,7 +190,13 @@
linkFlags = append(linkFlags, "--version-name ", versionName)
}
- return linkFlags, linkDeps, resDirs, overlayDirs, rroDirs, resourceZips
+ linkFlags, compileFlags = android.FilterList(linkFlags, []string{"--legacy"})
+
+ // Always set --pseudo-localize, it will be stripped out later for release
+ // builds that don't want it.
+ compileFlags = append(compileFlags, "--pseudo-localize")
+
+ return compileFlags, linkFlags, linkDeps, resDirs, overlayDirs, rroDirs, resourceZips
}
func (a *aapt) deps(ctx android.BottomUpMutatorContext, sdkDep sdkDep) {
@@ -220,7 +231,7 @@
a.mergedManifestFile = manifestPath
}
- linkFlags, linkDeps, resDirs, overlayDirs, rroDirs, resZips := a.aapt2Flags(ctx, sdkContext, manifestPath)
+ compileFlags, linkFlags, linkDeps, resDirs, overlayDirs, rroDirs, resZips := a.aapt2Flags(ctx, sdkContext, manifestPath)
rroDirs = append(rroDirs, staticRRODirs...)
linkFlags = append(linkFlags, libFlags...)
@@ -239,12 +250,12 @@
var compiledResDirs []android.Paths
for _, dir := range resDirs {
- compiledResDirs = append(compiledResDirs, aapt2Compile(ctx, dir.dir, dir.files).Paths())
+ compiledResDirs = append(compiledResDirs, aapt2Compile(ctx, dir.dir, dir.files, compileFlags).Paths())
}
for i, zip := range resZips {
flata := android.PathForModuleOut(ctx, fmt.Sprintf("reszip.%d.flata", i))
- aapt2CompileZip(ctx, flata, zip, "")
+ aapt2CompileZip(ctx, flata, zip, "", compileFlags)
compiledResDirs = append(compiledResDirs, android.Paths{flata})
}
@@ -273,7 +284,7 @@
}
for _, dir := range overlayDirs {
- compiledOverlay = append(compiledOverlay, aapt2Compile(ctx, dir.dir, dir.files).Paths()...)
+ compiledOverlay = append(compiledOverlay, aapt2Compile(ctx, dir.dir, dir.files, compileFlags).Paths()...)
}
var splitPackages android.WritablePaths
@@ -513,10 +524,6 @@
return a.sdkVersion()
}
-func (a *AARImport) noFrameworkLibs() bool {
- return false
-}
-
var _ AndroidLibraryDependency = (*AARImport)(nil)
func (a *AARImport) ExportPackage() android.Path {
@@ -598,9 +605,12 @@
},
})
+ // Always set --pseudo-localize, it will be stripped out later for release
+ // builds that don't want it.
+ compileFlags := []string{"--pseudo-localize"}
compiledResDir := android.PathForModuleOut(ctx, "flat-res")
flata := compiledResDir.Join(ctx, "gen_res.flata")
- aapt2CompileZip(ctx, flata, aar, "res")
+ aapt2CompileZip(ctx, flata, aar, "res", compileFlags)
a.exportPackage = android.PathForModuleOut(ctx, "package-res.apk")
srcJar := android.PathForModuleGen(ctx, "R.jar")
diff --git a/java/app.go b/java/app.go
index cab97de..f58b0f8 100644
--- a/java/app.go
+++ b/java/app.go
@@ -19,6 +19,7 @@
import (
"path/filepath"
"reflect"
+ "sort"
"strings"
"github.com/google/blueprint"
@@ -103,6 +104,10 @@
// Use_embedded_native_libs still selects whether they are stored uncompressed and aligned or compressed.
// True for android_test* modules.
AlwaysPackageNativeLibs bool `blueprint:"mutated"`
+
+ // If set, find and merge all NOTICE files that this module and its dependencies have and store
+ // it in the APK as an asset.
+ Embed_notices *bool
}
// android_app properties that can be overridden by override_android_app
@@ -224,15 +229,16 @@
return true
}
- if ctx.Config().UnbundledBuild() {
- return false
- }
-
- // Uncompress dex in APKs of privileged apps
+ // Uncompress dex in APKs of privileged apps (even for unbundled builds, they may
+ // be preinstalled as prebuilts).
if ctx.Config().UncompressPrivAppDex() && Bool(a.appProperties.Privileged) {
return true
}
+ if ctx.Config().UnbundledBuild() {
+ return false
+ }
+
return shouldUncompressDex(ctx, &a.dexpreopter)
}
@@ -351,6 +357,54 @@
return jniJarFile
}
+func (a *AndroidApp) noticeBuildActions(ctx android.ModuleContext, installDir android.OutputPath) android.OptionalPath {
+ if !Bool(a.appProperties.Embed_notices) && !ctx.Config().IsEnvTrue("ALWAYS_EMBED_NOTICES") {
+ return android.OptionalPath{}
+ }
+
+ // Collect NOTICE files from all dependencies.
+ seenModules := make(map[android.Module]bool)
+ noticePathSet := make(map[android.Path]bool)
+
+ ctx.WalkDeps(func(child android.Module, parent android.Module) bool {
+ // Have we already seen this?
+ if _, ok := seenModules[child]; ok {
+ return false
+ }
+ seenModules[child] = true
+
+ // Skip host modules.
+ if child.Target().Os.Class == android.Host || child.Target().Os.Class == android.HostCross {
+ return false
+ }
+
+ path := child.(android.Module).NoticeFile()
+ if path.Valid() {
+ noticePathSet[path.Path()] = true
+ }
+ return true
+ })
+
+ // If the app has one, add it too.
+ if a.NoticeFile().Valid() {
+ noticePathSet[a.NoticeFile().Path()] = true
+ }
+
+ if len(noticePathSet) == 0 {
+ return android.OptionalPath{}
+ }
+ var noticePaths []android.Path
+ for path := range noticePathSet {
+ noticePaths = append(noticePaths, path)
+ }
+ sort.Slice(noticePaths, func(i, j int) bool {
+ return noticePaths[i].String() < noticePaths[j].String()
+ })
+ noticeFile := android.BuildNoticeOutput(ctx, installDir, a.installApkName+".apk", noticePaths)
+
+ return android.OptionalPathForPath(noticeFile)
+}
+
// Reads and prepends a main cert from the default cert dir if it hasn't been set already, i.e. it
// isn't a cert module reference. Also checks and enforces system cert restriction if applicable.
func processMainCert(m android.ModuleBase, certPropValue string, certificates []Certificate, ctx android.ModuleContext) []Certificate {
@@ -391,6 +445,18 @@
// Check if the install APK name needs to be overridden.
a.installApkName = ctx.DeviceConfig().OverridePackageNameFor(a.Name())
+ var installDir android.OutputPath
+ if ctx.ModuleName() == "framework-res" {
+ // framework-res.apk is installed as system/framework/framework-res.apk
+ installDir = android.PathForModuleInstall(ctx, "framework")
+ } else if Bool(a.appProperties.Privileged) {
+ installDir = android.PathForModuleInstall(ctx, "priv-app", a.installApkName)
+ } else {
+ installDir = android.PathForModuleInstall(ctx, "app", a.installApkName)
+ }
+
+ a.aapt.noticeFile = a.noticeBuildActions(ctx, installDir)
+
// Process all building blocks, from AAPT to certificates.
a.aaptBuildActions(ctx)
@@ -432,16 +498,6 @@
a.bundleFile = bundleFile
// Install the app package.
- var installDir android.OutputPath
- if ctx.ModuleName() == "framework-res" {
- // framework-res.apk is installed as system/framework/framework-res.apk
- installDir = android.PathForModuleInstall(ctx, "framework")
- } else if Bool(a.appProperties.Privileged) {
- installDir = android.PathForModuleInstall(ctx, "priv-app", a.installApkName)
- } else {
- installDir = android.PathForModuleInstall(ctx, "app", a.installApkName)
- }
-
ctx.InstallFile(installDir, a.installApkName+".apk", a.outputFile)
for _, split := range a.aapt.splits {
ctx.InstallFile(installDir, a.installApkName+"_"+split.suffix+".apk", split.path)
diff --git a/java/app_test.go b/java/app_test.go
index 27802cd..c7ea338 100644
--- a/java/app_test.go
+++ b/java/app_test.go
@@ -546,79 +546,6 @@
}
}
-func TestJNIABI_no_framework_libs_true(t *testing.T) {
- ctx := testJava(t, cc.GatherRequiredDepsForTest(android.Android)+`
- cc_library {
- name: "libjni",
- system_shared_libs: [],
- stl: "none",
- }
-
- android_test {
- name: "test",
- no_framework_libs: true,
- jni_libs: ["libjni"],
- }
-
- android_test {
- name: "test_first",
- no_framework_libs: true,
- compile_multilib: "first",
- jni_libs: ["libjni"],
- }
-
- android_test {
- name: "test_both",
- no_framework_libs: true,
- compile_multilib: "both",
- jni_libs: ["libjni"],
- }
-
- android_test {
- name: "test_32",
- no_framework_libs: true,
- compile_multilib: "32",
- jni_libs: ["libjni"],
- }
-
- android_test {
- name: "test_64",
- no_framework_libs: true,
- compile_multilib: "64",
- jni_libs: ["libjni"],
- }
- `)
-
- testCases := []struct {
- name string
- abis []string
- }{
- {"test", []string{"arm64-v8a"}},
- {"test_first", []string{"arm64-v8a"}},
- {"test_both", []string{"arm64-v8a", "armeabi-v7a"}},
- {"test_32", []string{"armeabi-v7a"}},
- {"test_64", []string{"arm64-v8a"}},
- }
-
- for _, test := range testCases {
- t.Run(test.name, func(t *testing.T) {
- app := ctx.ModuleForTests(test.name, "android_common")
- jniLibZip := app.Output("jnilibs.zip")
- var abis []string
- args := strings.Fields(jniLibZip.Args["jarArgs"])
- for i := 0; i < len(args); i++ {
- if args[i] == "-P" {
- abis = append(abis, filepath.Base(args[i+1]))
- i++
- }
- }
- if !reflect.DeepEqual(abis, test.abis) {
- t.Errorf("want abis %v, got %v", test.abis, abis)
- }
- })
- }
-}
-
func TestJNIABI(t *testing.T) {
ctx := testJava(t, cc.GatherRequiredDepsForTest(android.Android)+`
cc_library {
@@ -692,89 +619,6 @@
}
}
-func TestJNIPackaging_no_framework_libs_true(t *testing.T) {
- ctx := testJava(t, cc.GatherRequiredDepsForTest(android.Android)+`
- cc_library {
- name: "libjni",
- system_shared_libs: [],
- stl: "none",
- }
-
- android_app {
- name: "app",
- jni_libs: ["libjni"],
- }
-
- android_app {
- name: "app_noembed",
- jni_libs: ["libjni"],
- use_embedded_native_libs: false,
- }
-
- android_app {
- name: "app_embed",
- jni_libs: ["libjni"],
- use_embedded_native_libs: true,
- }
-
- android_test {
- name: "test",
- no_framework_libs: true,
- jni_libs: ["libjni"],
- }
-
- android_test {
- name: "test_noembed",
- no_framework_libs: true,
- jni_libs: ["libjni"],
- use_embedded_native_libs: false,
- }
-
- android_test_helper_app {
- name: "test_helper",
- no_framework_libs: true,
- jni_libs: ["libjni"],
- }
-
- android_test_helper_app {
- name: "test_helper_noembed",
- no_framework_libs: true,
- jni_libs: ["libjni"],
- use_embedded_native_libs: false,
- }
- `)
-
- testCases := []struct {
- name string
- packaged bool
- compressed bool
- }{
- {"app", false, false},
- {"app_noembed", false, false},
- {"app_embed", true, false},
- {"test", true, false},
- {"test_noembed", true, true},
- {"test_helper", true, false},
- {"test_helper_noembed", true, true},
- }
-
- for _, test := range testCases {
- t.Run(test.name, func(t *testing.T) {
- app := ctx.ModuleForTests(test.name, "android_common")
- jniLibZip := app.MaybeOutput("jnilibs.zip")
- if g, w := (jniLibZip.Rule != nil), test.packaged; g != w {
- t.Errorf("expected jni packaged %v, got %v", w, g)
- }
-
- if jniLibZip.Rule != nil {
- if g, w := !strings.Contains(jniLibZip.Args["jarArgs"], "-L 0"), test.compressed; g != w {
- t.Errorf("expected jni compressed %v, got %v", w, g)
- }
- }
- })
- }
-}
-
func TestJNIPackaging(t *testing.T) {
ctx := testJava(t, cc.GatherRequiredDepsForTest(android.Android)+`
cc_library {
@@ -1552,3 +1396,181 @@
})
}
}
+
+func TestEmbedNotice(t *testing.T) {
+ ctx := testJava(t, cc.GatherRequiredDepsForTest(android.Android)+`
+ android_app {
+ name: "foo",
+ srcs: ["a.java"],
+ static_libs: ["javalib"],
+ jni_libs: ["libjni"],
+ notice: "APP_NOTICE",
+ embed_notices: true,
+ }
+
+ // No embed_notice flag
+ android_app {
+ name: "bar",
+ srcs: ["a.java"],
+ jni_libs: ["libjni"],
+ notice: "APP_NOTICE",
+ }
+
+ // No NOTICE files
+ android_app {
+ name: "baz",
+ srcs: ["a.java"],
+ embed_notices: true,
+ }
+
+ cc_library {
+ name: "libjni",
+ system_shared_libs: [],
+ stl: "none",
+ notice: "LIB_NOTICE",
+ }
+
+ java_library {
+ name: "javalib",
+ srcs: [
+ ":gen",
+ ],
+ }
+
+ genrule {
+ name: "gen",
+ tools: ["gentool"],
+ out: ["gen.java"],
+ notice: "GENRULE_NOTICE",
+ }
+
+ java_binary_host {
+ name: "gentool",
+ srcs: ["b.java"],
+ notice: "TOOL_NOTICE",
+ }
+ `)
+
+ // foo has NOTICE files to process, and embed_notices is true.
+ foo := ctx.ModuleForTests("foo", "android_common")
+ // verify merge notices rule.
+ mergeNotices := foo.Rule("mergeNoticesRule")
+ noticeInputs := mergeNotices.Inputs.Strings()
+ // TOOL_NOTICE should be excluded as it's a host module.
+ if len(mergeNotices.Inputs) != 3 {
+ t.Errorf("number of input notice files: expected = 3, actual = %q", noticeInputs)
+ }
+ if !inList("APP_NOTICE", noticeInputs) {
+ t.Errorf("APP_NOTICE is missing from notice files, %q", noticeInputs)
+ }
+ if !inList("LIB_NOTICE", noticeInputs) {
+ t.Errorf("LIB_NOTICE is missing from notice files, %q", noticeInputs)
+ }
+ if !inList("GENRULE_NOTICE", noticeInputs) {
+ t.Errorf("GENRULE_NOTICE is missing from notice files, %q", noticeInputs)
+ }
+ // aapt2 flags should include -A <NOTICE dir> so that its contents are put in the APK's /assets.
+ res := foo.Output("package-res.apk")
+ aapt2Flags := res.Args["flags"]
+ e := "-A " + buildDir + "/.intermediates/foo/android_common/NOTICE"
+ if !strings.Contains(aapt2Flags, e) {
+ t.Errorf("asset dir flag for NOTICE, %q is missing in aapt2 link flags, %q", e, aapt2Flags)
+ }
+
+ // bar has NOTICE files to process, but embed_notices is not set.
+ bar := ctx.ModuleForTests("bar", "android_common")
+ mergeNotices = bar.MaybeRule("mergeNoticesRule")
+ if mergeNotices.Rule != nil {
+ t.Errorf("mergeNotices shouldn't have run for bar")
+ }
+
+ // baz's embed_notice is true, but it doesn't have any NOTICE files.
+ baz := ctx.ModuleForTests("baz", "android_common")
+ mergeNotices = baz.MaybeRule("mergeNoticesRule")
+ if mergeNotices.Rule != nil {
+ t.Errorf("mergeNotices shouldn't have run for baz")
+ }
+}
+
+func TestUncompressDex(t *testing.T) {
+ testCases := []struct {
+ name string
+ bp string
+
+ uncompressedPlatform bool
+ uncompressedUnbundled bool
+ }{
+ {
+ name: "normal",
+ bp: `
+ android_app {
+ name: "foo",
+ srcs: ["a.java"],
+ }
+ `,
+ uncompressedPlatform: true,
+ uncompressedUnbundled: false,
+ },
+ {
+ name: "use_embedded_dex",
+ bp: `
+ android_app {
+ name: "foo",
+ use_embedded_dex: true,
+ srcs: ["a.java"],
+ }
+ `,
+ uncompressedPlatform: true,
+ uncompressedUnbundled: true,
+ },
+ {
+ name: "privileged",
+ bp: `
+ android_app {
+ name: "foo",
+ privileged: true,
+ srcs: ["a.java"],
+ }
+ `,
+ uncompressedPlatform: true,
+ uncompressedUnbundled: true,
+ },
+ }
+
+ test := func(t *testing.T, bp string, want bool, unbundled bool) {
+ t.Helper()
+
+ config := testConfig(nil)
+ if unbundled {
+ config.TestProductVariables.Unbundled_build = proptools.BoolPtr(true)
+ }
+
+ ctx := testAppContext(config, bp, nil)
+
+ run(t, ctx, config)
+
+ foo := ctx.ModuleForTests("foo", "android_common")
+ dex := foo.Rule("r8")
+ uncompressedInDexJar := strings.Contains(dex.Args["zipFlags"], "-L 0")
+ aligned := foo.MaybeRule("zipalign").Rule != nil
+
+ if uncompressedInDexJar != want {
+ t.Errorf("want uncompressed in dex %v, got %v", want, uncompressedInDexJar)
+ }
+
+ if aligned != want {
+ t.Errorf("want aligned %v, got %v", want, aligned)
+ }
+ }
+
+ for _, tt := range testCases {
+ t.Run(tt.name, func(t *testing.T) {
+ t.Run("platform", func(t *testing.T) {
+ test(t, tt.bp, tt.uncompressedPlatform, false)
+ })
+ t.Run("unbundled", func(t *testing.T) {
+ test(t, tt.bp, tt.uncompressedUnbundled, true)
+ })
+ })
+ }
+}
diff --git a/java/device_host_converter_test.go b/java/device_host_converter_test.go
index 146bf6f..9b9d0d8 100644
--- a/java/device_host_converter_test.go
+++ b/java/device_host_converter_test.go
@@ -126,7 +126,7 @@
java_library {
name: "device_module",
- no_framework_libs: true,
+ sdk_version: "core_platform",
srcs: ["b.java"],
java_resources: ["java-res/b/b"],
static_libs: ["host_for_device_module"],
diff --git a/java/dexpreopt_bootjars.go b/java/dexpreopt_bootjars.go
index eb735c1..fe468a9 100644
--- a/java/dexpreopt_bootjars.go
+++ b/java/dexpreopt_bootjars.go
@@ -286,8 +286,6 @@
if profile != nil {
cmd.FlagWithArg("--compiler-filter=", "speed-profile")
cmd.FlagWithInput("--profile-file=", profile)
- } else if global.PreloadedClasses.Valid() {
- cmd.FlagWithInput("--image-classes=", global.PreloadedClasses.Path())
}
if global.DirtyImageObjects.Valid() {
@@ -374,7 +372,7 @@
func bootImageProfileRule(ctx android.SingletonContext, image *bootImage, missingDeps []string) android.WritablePath {
global := dexpreoptGlobalConfig(ctx)
- if !global.UseProfileForBootImage || ctx.Config().IsPdkBuild() || ctx.Config().UnbundledBuild() {
+ if global.DisableGenerateProfile || ctx.Config().IsPdkBuild() || ctx.Config().UnbundledBuild() {
return nil
}
return ctx.Config().Once(bootImageProfileRuleKey, func() interface{} {
diff --git a/java/dexpreopt_bootjars_test.go b/java/dexpreopt_bootjars_test.go
index cbb52f1..f91ff69 100644
--- a/java/dexpreopt_bootjars_test.go
+++ b/java/dexpreopt_bootjars_test.go
@@ -62,6 +62,7 @@
bootArt := dexpreoptBootJars.Output("boot.art")
expectedInputs := []string{
+ "dex_bootjars/boot.prof",
"dex_bootjars_input/foo.jar",
"dex_bootjars_input/bar.jar",
"dex_bootjars_input/baz.jar",
diff --git a/java/dexpreopt_config.go b/java/dexpreopt_config.go
index c396d3e..4a4d6d5 100644
--- a/java/dexpreopt_config.go
+++ b/java/dexpreopt_config.go
@@ -49,7 +49,8 @@
return ctx.Config().Once(dexpreoptTestGlobalConfigKey, func() interface{} {
// Nope, return a config with preopting disabled
return globalConfigAndRaw{dexpreopt.GlobalConfig{
- DisablePreopt: true,
+ DisablePreopt: true,
+ DisableGenerateProfile: true,
}, nil}
})
}).(globalConfigAndRaw)
diff --git a/java/droiddoc.go b/java/droiddoc.go
index 18f337a..25101de 100644
--- a/java/droiddoc.go
+++ b/java/droiddoc.go
@@ -172,9 +172,6 @@
// list of java libraries that will be in the classpath.
Libs []string `android:"arch_variant"`
- // don't build against the framework libraries (ext, and framework for device targets)
- No_framework_libs *bool
-
// the java library (in classpath) for documentation that provides java srcs and srcjars.
Srcs_lib *string
@@ -535,10 +532,6 @@
return j.sdkVersion()
}
-func (j *Javadoc) noFrameworkLibs() bool {
- return Bool(j.properties.No_framework_libs)
-}
-
func (j *Javadoc) addDeps(ctx android.BottomUpMutatorContext) {
if ctx.Device() {
sdkDep := decodeSdkDep(ctx, sdkContext(j))
diff --git a/java/java.go b/java/java.go
index 0378436..7c84e76 100644
--- a/java/java.go
+++ b/java/java.go
@@ -82,9 +82,6 @@
// list of files that should be excluded from java_resources and java_resource_dirs
Exclude_java_resources []string `android:"path,arch_variant"`
- // don't build against the framework libraries (ext, and framework for device targets)
- No_framework_libs *bool
-
// list of module-specific flags that will be used for javac compiles
Javacflags []string `android:"arch_variant"`
@@ -494,10 +491,6 @@
return j.sdkVersion()
}
-func (j *Module) noFrameworkLibs() bool {
- return Bool(j.properties.No_framework_libs)
-}
-
func (j *Module) deps(ctx android.BottomUpMutatorContext) {
if ctx.Device() {
sdkDep := decodeSdkDep(ctx, sdkContext(j))
@@ -518,7 +511,7 @@
}
} else if j.deviceProperties.System_modules == nil {
ctx.PropertyErrorf("sdk_version",
- `system_modules is required to be set when sdk_version is "none", did you mean no_framework_libs?`)
+ `system_modules is required to be set when sdk_version is "none", did you mean "core_platform"`)
} else if *j.deviceProperties.System_modules != "none" {
ctx.AddVariationDependencies(nil, systemModulesTag, *j.deviceProperties.System_modules)
}
diff --git a/java/java_test.go b/java/java_test.go
index 628fec3..677174d 100644
--- a/java/java_test.go
+++ b/java/java_test.go
@@ -121,6 +121,10 @@
"b.kt": nil,
"a.jar": nil,
"b.jar": nil,
+ "APP_NOTICE": nil,
+ "GENRULE_NOTICE": nil,
+ "LIB_NOTICE": nil,
+ "TOOL_NOTICE": nil,
"java-res/a/a": nil,
"java-res/b/b": nil,
"java-res2/a": nil,
diff --git a/java/sdk.go b/java/sdk.go
index 6ffe399..7b79a49 100644
--- a/java/sdk.go
+++ b/java/sdk.go
@@ -44,9 +44,6 @@
minSdkVersion() string
// targetSdkVersion returns the target_sdk_version property of the current module, or sdkVersion() if it is not set.
targetSdkVersion() string
-
- // Temporarily provide access to the no_frameworks_libs property (where present).
- noFrameworkLibs() bool
}
func sdkVersionOrDefault(ctx android.BaseModuleContext, v string) string {
@@ -84,6 +81,7 @@
func decodeSdkDep(ctx android.BaseModuleContext, sdkContext sdkContext) sdkDep {
v := sdkContext.sdkVersion()
+
// For PDK builds, use the latest SDK version instead of "current"
if ctx.Config().IsPdkBuild() && (v == "" || v == "current") {
sdkVersions := ctx.Config().Get(sdkVersionsKey).([]int)
@@ -141,9 +139,6 @@
useFiles: true,
jars: android.Paths{jarPath.Path(), lambdaStubsPath},
aidl: android.OptionalPathForPath(aidlPath.Path()),
-
- // Pass value straight through for now to match previous behavior.
- noFrameworksLibs: sdkContext.noFrameworkLibs(),
}
}
@@ -154,15 +149,12 @@
systemModules: m + "_system_modules",
frameworkResModule: r,
aidl: android.OptionalPathForPath(aidl),
-
- // Pass value straight through for now to match previous behavior.
- noFrameworksLibs: sdkContext.noFrameworkLibs(),
}
if m == "core.current.stubs" {
ret.systemModules = "core-current-stubs-system-modules"
- } else if m == "core.platform.api.stubs" {
- ret.systemModules = "core-platform-api-stubs-system-modules"
+ // core_current does not include framework classes.
+ ret.noFrameworksLibs = true
}
return ret
}
@@ -192,9 +184,6 @@
return sdkDep{
useDefaultLibs: true,
frameworkResModule: "framework-res",
-
- // Pass value straight through for now to match previous behavior.
- noFrameworksLibs: sdkContext.noFrameworkLibs(),
}
case "none":
return sdkDep{
diff --git a/java/sdk_test.go b/java/sdk_test.go
index 953c372..f82a4fb 100644
--- a/java/sdk_test.go
+++ b/java/sdk_test.go
@@ -47,14 +47,6 @@
aidl: "-Iframework/aidl",
},
{
- name: "no_framework_libs:true",
- properties: `no_framework_libs:true`,
- bootclasspath: config.DefaultBootclasspathLibraries,
- system: config.DefaultSystemModules,
- classpath: []string{},
- aidl: "",
- },
- {
name: `sdk_version:"core_platform"`,
properties: `sdk_version:"core_platform"`,
bootclasspath: config.DefaultBootclasspathLibraries,
diff --git a/java/system_modules.go b/java/system_modules.go
index f71f452..c616249 100644
--- a/java/system_modules.go
+++ b/java/system_modules.go
@@ -138,7 +138,7 @@
fmt.Fprintln(w, makevar, ":=$=", strings.Join(system.properties.Libs, " "))
fmt.Fprintln(w)
- makevar = "SOONG_SYSTEM_MODULE_DEPS_" + name
+ makevar = "SOONG_SYSTEM_MODULES_DEPS_" + name
fmt.Fprintln(w, makevar, ":=$=", strings.Join(system.outputDeps.Strings(), " "))
fmt.Fprintln(w)
diff --git a/ui/build/config.go b/ui/build/config.go
index 6df9529..4a70f06 100644
--- a/ui/build/config.go
+++ b/ui/build/config.go
@@ -71,6 +71,9 @@
// Builds all of the modules and their dependencies of a list of specified directories. All specified
// directories are relative to the root directory of the source tree.
BUILD_MODULES_IN_DIRECTORIES
+
+ // Build a list of specified modules. If none was specified, simply build the whole source tree.
+ BUILD_MODULES
)
// checkTopDir validates that the current directory is at the root directory of the source tree.
@@ -290,6 +293,8 @@
var targets []string
switch action {
+ case BUILD_MODULES:
+ // No additional processing is required when building a list of specific modules or all modules.
case BUILD_MODULES_IN_A_DIRECTORY:
// If dir is the root source tree, all the modules are built of the source tree are built so
// no need to find the build file.
diff --git a/ui/build/config_test.go b/ui/build/config_test.go
index 1ef5456..856af11 100644
--- a/ui/build/config_test.go
+++ b/ui/build/config_test.go
@@ -763,6 +763,51 @@
}
}
+func TestGetConfigArgsBuildModules(t *testing.T) {
+ tests := []buildActionTestCase{{
+ description: "normal execution from the root source tree directory",
+ dirsInTrees: []string{"0/1/2", "0/2", "0/3"},
+ buildFiles: []string{"0/1/2/Android.mk", "0/2/Android.bp", "0/3/Android.mk"},
+ args: []string{"-j", "fake_module", "fake_module2"},
+ curDir: ".",
+ tidyOnly: "",
+ expectedArgs: []string{"-j", "fake_module", "fake_module2"},
+ expectedEnvVars: []envVar{},
+ }, {
+ description: "normal execution in deep directory",
+ dirsInTrees: []string{"0/1/2", "0/2", "0/3", "1/2/3/4/5/6/7/8/9/1/2/3/4/5/6"},
+ buildFiles: []string{"0/1/2/Android.mk", "0/2/Android.bp", "1/2/3/4/5/6/7/8/9/1/2/3/4/5/6/Android.mk"},
+ args: []string{"-j", "fake_module", "fake_module2", "-k"},
+ curDir: "1/2/3/4/5/6/7/8/9",
+ tidyOnly: "",
+ expectedArgs: []string{"-j", "fake_module", "fake_module2", "-k"},
+ expectedEnvVars: []envVar{},
+ }, {
+ description: "normal execution in deep directory, no targets",
+ dirsInTrees: []string{"0/1/2", "0/2", "0/3", "1/2/3/4/5/6/7/8/9/1/2/3/4/5/6"},
+ buildFiles: []string{"0/1/2/Android.mk", "0/2/Android.bp", "1/2/3/4/5/6/7/8/9/1/2/3/4/5/6/Android.mk"},
+ args: []string{"-j", "-k"},
+ curDir: "1/2/3/4/5/6/7/8/9",
+ tidyOnly: "",
+ expectedArgs: []string{"-j", "-k"},
+ expectedEnvVars: []envVar{},
+ }, {
+ description: "normal execution in root source tree, no args",
+ dirsInTrees: []string{"0/1/2", "0/2", "0/3"},
+ buildFiles: []string{"0/1/2/Android.mk", "0/2/Android.bp"},
+ args: []string{},
+ curDir: "1/2/3/4/5/6/7/8/9",
+ tidyOnly: "",
+ expectedArgs: []string{},
+ expectedEnvVars: []envVar{},
+ }}
+ for _, tt := range tests {
+ t.Run("build action BUILD_MODULES with dependencies, "+tt.description, func(t *testing.T) {
+ testGetConfigArgs(t, tt, BUILD_MODULES, true)
+ })
+ }
+}
+
// TODO: Remove this test case once mm shell build command has been deprecated.
func TestGetConfigArgsBuildModulesInDirecotoryNoDeps(t *testing.T) {
tests := []buildActionTestCase{{
diff --git a/ui/build/paths/config.go b/ui/build/paths/config.go
index c9946e2..e2c5043 100644
--- a/ui/build/paths/config.go
+++ b/ui/build/paths/config.go
@@ -88,13 +88,14 @@
"getopt": Allowed,
"git": Allowed,
"grep": Allowed,
+ "gzcat": Allowed,
"gzip": Allowed,
"hexdump": Allowed,
"jar": Allowed,
"java": Allowed,
"javap": Allowed,
"lsof": Allowed,
- "m4": Allowed,
+ "m4": Log,
"openssl": Allowed,
"patch": Allowed,
"pstree": Allowed,
diff --git a/ui/status/Android.bp b/ui/status/Android.bp
index 901a713..ec929b3 100644
--- a/ui/status/Android.bp
+++ b/ui/status/Android.bp
@@ -19,14 +19,17 @@
"golang-protobuf-proto",
"soong-ui-logger",
"soong-ui-status-ninja_frontend",
+ "soong-ui-status-build_error_proto",
],
srcs: [
+ "critical_path.go",
"kati.go",
"log.go",
"ninja.go",
"status.go",
],
testSrcs: [
+ "critical_path_test.go",
"kati_test.go",
"ninja_test.go",
"status_test.go",
@@ -41,3 +44,12 @@
"ninja_frontend/frontend.pb.go",
],
}
+
+bootstrap_go_package {
+ name: "soong-ui-status-build_error_proto",
+ pkgPath: "android/soong/ui/status/build_error_proto",
+ deps: ["golang-protobuf-proto"],
+ srcs: [
+ "build_error_proto/build_error.pb.go",
+ ],
+}
diff --git a/ui/status/build_error_proto/build_error.pb.go b/ui/status/build_error_proto/build_error.pb.go
new file mode 100644
index 0000000..d4d0a6e
--- /dev/null
+++ b/ui/status/build_error_proto/build_error.pb.go
@@ -0,0 +1,175 @@
+// Code generated by protoc-gen-go. DO NOT EDIT.
+// source: build_error.proto
+
+package soong_build_error_proto
+
+import (
+ fmt "fmt"
+ proto "github.com/golang/protobuf/proto"
+ math "math"
+)
+
+// Reference imports to suppress errors if they are not otherwise used.
+var _ = proto.Marshal
+var _ = fmt.Errorf
+var _ = math.Inf
+
+// This is a compile-time assertion to ensure that this generated file
+// is compatible with the proto package it is being compiled against.
+// A compilation error at this line likely means your copy of the
+// proto package needs to be updated.
+const _ = proto.ProtoPackageIsVersion3 // please upgrade the proto package
+
+type BuildError struct {
+ // List of error messages of the overall build. The error messages
+ // are not associated with a build action.
+ ErrorMessages []string `protobuf:"bytes,1,rep,name=error_messages,json=errorMessages" json:"error_messages,omitempty"`
+ // List of build action errors.
+ ActionErrors []*BuildActionError `protobuf:"bytes,2,rep,name=action_errors,json=actionErrors" json:"action_errors,omitempty"`
+ XXX_NoUnkeyedLiteral struct{} `json:"-"`
+ XXX_unrecognized []byte `json:"-"`
+ XXX_sizecache int32 `json:"-"`
+}
+
+func (m *BuildError) Reset() { *m = BuildError{} }
+func (m *BuildError) String() string { return proto.CompactTextString(m) }
+func (*BuildError) ProtoMessage() {}
+func (*BuildError) Descriptor() ([]byte, []int) {
+ return fileDescriptor_a2e15b05802a5501, []int{0}
+}
+
+func (m *BuildError) XXX_Unmarshal(b []byte) error {
+ return xxx_messageInfo_BuildError.Unmarshal(m, b)
+}
+func (m *BuildError) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
+ return xxx_messageInfo_BuildError.Marshal(b, m, deterministic)
+}
+func (m *BuildError) XXX_Merge(src proto.Message) {
+ xxx_messageInfo_BuildError.Merge(m, src)
+}
+func (m *BuildError) XXX_Size() int {
+ return xxx_messageInfo_BuildError.Size(m)
+}
+func (m *BuildError) XXX_DiscardUnknown() {
+ xxx_messageInfo_BuildError.DiscardUnknown(m)
+}
+
+var xxx_messageInfo_BuildError proto.InternalMessageInfo
+
+func (m *BuildError) GetErrorMessages() []string {
+ if m != nil {
+ return m.ErrorMessages
+ }
+ return nil
+}
+
+func (m *BuildError) GetActionErrors() []*BuildActionError {
+ if m != nil {
+ return m.ActionErrors
+ }
+ return nil
+}
+
+// Build is composed of a list of build action. There can be a set of build
+// actions that can failed.
+type BuildActionError struct {
+ // Description of the command.
+ Description *string `protobuf:"bytes,1,opt,name=description" json:"description,omitempty"`
+ // The command name that raised the error.
+ Command *string `protobuf:"bytes,2,opt,name=command" json:"command,omitempty"`
+ // The command output stream.
+ Output *string `protobuf:"bytes,3,opt,name=output" json:"output,omitempty"`
+ // List of artifacts (i.e. files) that was produced by the command.
+ Artifacts []string `protobuf:"bytes,4,rep,name=artifacts" json:"artifacts,omitempty"`
+ // The error string produced by the build action.
+ Error *string `protobuf:"bytes,5,opt,name=error" json:"error,omitempty"`
+ XXX_NoUnkeyedLiteral struct{} `json:"-"`
+ XXX_unrecognized []byte `json:"-"`
+ XXX_sizecache int32 `json:"-"`
+}
+
+func (m *BuildActionError) Reset() { *m = BuildActionError{} }
+func (m *BuildActionError) String() string { return proto.CompactTextString(m) }
+func (*BuildActionError) ProtoMessage() {}
+func (*BuildActionError) Descriptor() ([]byte, []int) {
+ return fileDescriptor_a2e15b05802a5501, []int{1}
+}
+
+func (m *BuildActionError) XXX_Unmarshal(b []byte) error {
+ return xxx_messageInfo_BuildActionError.Unmarshal(m, b)
+}
+func (m *BuildActionError) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
+ return xxx_messageInfo_BuildActionError.Marshal(b, m, deterministic)
+}
+func (m *BuildActionError) XXX_Merge(src proto.Message) {
+ xxx_messageInfo_BuildActionError.Merge(m, src)
+}
+func (m *BuildActionError) XXX_Size() int {
+ return xxx_messageInfo_BuildActionError.Size(m)
+}
+func (m *BuildActionError) XXX_DiscardUnknown() {
+ xxx_messageInfo_BuildActionError.DiscardUnknown(m)
+}
+
+var xxx_messageInfo_BuildActionError proto.InternalMessageInfo
+
+func (m *BuildActionError) GetDescription() string {
+ if m != nil && m.Description != nil {
+ return *m.Description
+ }
+ return ""
+}
+
+func (m *BuildActionError) GetCommand() string {
+ if m != nil && m.Command != nil {
+ return *m.Command
+ }
+ return ""
+}
+
+func (m *BuildActionError) GetOutput() string {
+ if m != nil && m.Output != nil {
+ return *m.Output
+ }
+ return ""
+}
+
+func (m *BuildActionError) GetArtifacts() []string {
+ if m != nil {
+ return m.Artifacts
+ }
+ return nil
+}
+
+func (m *BuildActionError) GetError() string {
+ if m != nil && m.Error != nil {
+ return *m.Error
+ }
+ return ""
+}
+
+func init() {
+ proto.RegisterType((*BuildError)(nil), "soong_build_error.BuildError")
+ proto.RegisterType((*BuildActionError)(nil), "soong_build_error.BuildActionError")
+}
+
+func init() { proto.RegisterFile("build_error.proto", fileDescriptor_a2e15b05802a5501) }
+
+var fileDescriptor_a2e15b05802a5501 = []byte{
+ // 229 bytes of a gzipped FileDescriptorProto
+ 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x64, 0x90, 0xc1, 0x4a, 0xc3, 0x40,
+ 0x10, 0x86, 0x49, 0x63, 0x95, 0x4c, 0xad, 0xd8, 0x41, 0x74, 0x04, 0x0f, 0xa1, 0x22, 0xe4, 0x94,
+ 0x83, 0x6f, 0x60, 0x41, 0xf0, 0xe2, 0x25, 0x47, 0x2f, 0x61, 0xdd, 0xac, 0x65, 0xc1, 0x64, 0xc2,
+ 0xce, 0xe6, 0xe8, 0x8b, 0xf8, 0xb4, 0x92, 0x69, 0xa5, 0xa5, 0x39, 0x7e, 0xdf, 0x3f, 0xfb, 0xef,
+ 0xce, 0xc2, 0xea, 0x73, 0xf0, 0xdf, 0x4d, 0xed, 0x42, 0xe0, 0x50, 0xf6, 0x81, 0x23, 0xe3, 0x4a,
+ 0x98, 0xbb, 0x6d, 0x7d, 0x14, 0xac, 0x7f, 0x00, 0x36, 0x23, 0xbe, 0x8e, 0x84, 0x4f, 0x70, 0xa5,
+ 0xba, 0x6e, 0x9d, 0x88, 0xd9, 0x3a, 0xa1, 0x24, 0x4f, 0x8b, 0xac, 0x5a, 0xaa, 0x7d, 0xdf, 0x4b,
+ 0x7c, 0x83, 0xa5, 0xb1, 0xd1, 0x73, 0xb7, 0x2b, 0x11, 0x9a, 0xe5, 0x69, 0xb1, 0x78, 0x7e, 0x2c,
+ 0x27, 0xfd, 0xa5, 0x96, 0xbf, 0xe8, 0xb0, 0x5e, 0x51, 0x5d, 0x9a, 0x03, 0xc8, 0xfa, 0x37, 0x81,
+ 0xeb, 0xd3, 0x11, 0xcc, 0x61, 0xd1, 0x38, 0xb1, 0xc1, 0xf7, 0xa3, 0xa3, 0x24, 0x4f, 0x8a, 0xac,
+ 0x3a, 0x56, 0x48, 0x70, 0x61, 0xb9, 0x6d, 0x4d, 0xd7, 0xd0, 0x4c, 0xd3, 0x7f, 0xc4, 0x5b, 0x38,
+ 0xe7, 0x21, 0xf6, 0x43, 0xa4, 0x54, 0x83, 0x3d, 0xe1, 0x03, 0x64, 0x26, 0x44, 0xff, 0x65, 0x6c,
+ 0x14, 0x3a, 0xd3, 0xa5, 0x0e, 0x02, 0x6f, 0x60, 0xae, 0xcf, 0xa5, 0xb9, 0x1e, 0xda, 0xc1, 0xe6,
+ 0xfe, 0xe3, 0x6e, 0xb2, 0x50, 0xad, 0x3f, 0xf9, 0x17, 0x00, 0x00, 0xff, 0xff, 0xb6, 0x18, 0x9e,
+ 0x17, 0x5d, 0x01, 0x00, 0x00,
+}
diff --git a/ui/status/build_error_proto/build_error.proto b/ui/status/build_error_proto/build_error.proto
new file mode 100644
index 0000000..9c8470d
--- /dev/null
+++ b/ui/status/build_error_proto/build_error.proto
@@ -0,0 +1,46 @@
+// Copyright 2019 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.
+
+syntax = "proto2";
+
+package soong_build_error;
+option go_package = "soong_build_error_proto";
+
+message BuildError {
+ // List of error messages of the overall build. The error messages
+ // are not associated with a build action.
+ repeated string error_messages = 1;
+
+ // List of build action errors.
+ repeated BuildActionError action_errors = 2;
+}
+
+// Build is composed of a list of build action. There can be a set of build
+// actions that can failed.
+message BuildActionError {
+ // Description of the command.
+ optional string description = 1;
+
+ // The command name that raised the error.
+ optional string command = 2;
+
+ // The command output stream.
+ optional string output = 3;
+
+ // List of artifacts (i.e. files) that was produced by the command.
+ repeated string artifacts = 4;
+
+ // The error string produced by the build action.
+ optional string error = 5;
+}
diff --git a/ui/status/build_error_proto/regen.sh b/ui/status/build_error_proto/regen.sh
new file mode 100755
index 0000000..7c3ec8f
--- /dev/null
+++ b/ui/status/build_error_proto/regen.sh
@@ -0,0 +1,3 @@
+#!/bin/bash
+
+aprotoc --go_out=paths=source_relative:. build_error.proto
diff --git a/ui/status/critical_path.go b/ui/status/critical_path.go
new file mode 100644
index 0000000..444327b
--- /dev/null
+++ b/ui/status/critical_path.go
@@ -0,0 +1,152 @@
+// Copyright 2019 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 status
+
+import (
+ "time"
+
+ "android/soong/ui/logger"
+)
+
+func NewCriticalPath(log logger.Logger) StatusOutput {
+ return &criticalPath{
+ log: log,
+ running: make(map[*Action]time.Time),
+ nodes: make(map[string]*node),
+ clock: osClock{},
+ }
+}
+
+type criticalPath struct {
+ log logger.Logger
+
+ nodes map[string]*node
+ running map[*Action]time.Time
+
+ start, end time.Time
+
+ clock clock
+}
+
+type clock interface {
+ Now() time.Time
+}
+
+type osClock struct{}
+
+func (osClock) Now() time.Time { return time.Now() }
+
+// A critical path node stores the critical path (the minimum time to build the node and all of its dependencies given
+// perfect parallelism) for an node.
+type node struct {
+ action *Action
+ cumulativeDuration time.Duration
+ duration time.Duration
+ input *node
+}
+
+func (cp *criticalPath) StartAction(action *Action, counts Counts) {
+ start := cp.clock.Now()
+ if cp.start.IsZero() {
+ cp.start = start
+ }
+ cp.running[action] = start
+}
+
+func (cp *criticalPath) FinishAction(result ActionResult, counts Counts) {
+ if start, ok := cp.running[result.Action]; ok {
+ delete(cp.running, result.Action)
+
+ // Determine the input to this edge with the longest cumulative duration
+ var criticalPathInput *node
+ for _, input := range result.Action.Inputs {
+ if x := cp.nodes[input]; x != nil {
+ if criticalPathInput == nil || x.cumulativeDuration > criticalPathInput.cumulativeDuration {
+ criticalPathInput = x
+ }
+ }
+ }
+
+ end := cp.clock.Now()
+ duration := end.Sub(start)
+
+ cumulativeDuration := duration
+ if criticalPathInput != nil {
+ cumulativeDuration += criticalPathInput.cumulativeDuration
+ }
+
+ node := &node{
+ action: result.Action,
+ cumulativeDuration: cumulativeDuration,
+ duration: duration,
+ input: criticalPathInput,
+ }
+
+ for _, output := range result.Action.Outputs {
+ cp.nodes[output] = node
+ }
+
+ cp.end = end
+ }
+}
+
+func (cp *criticalPath) Flush() {
+ criticalPath := cp.criticalPath()
+
+ if len(criticalPath) > 0 {
+ // Log the critical path to the verbose log
+ criticalTime := criticalPath[0].cumulativeDuration.Round(time.Second)
+ cp.log.Verbosef("critical path took %s", criticalTime.String())
+ if !cp.start.IsZero() {
+ elapsedTime := cp.end.Sub(cp.start).Round(time.Second)
+ cp.log.Verbosef("elapsed time %s", elapsedTime.String())
+ cp.log.Verbosef("perfect parallelism ratio %d%%",
+ int(float64(criticalTime)/float64(elapsedTime)*100))
+ }
+ cp.log.Verbose("critical path:")
+ for i := len(criticalPath) - 1; i >= 0; i-- {
+ duration := criticalPath[i].duration
+ duration = duration.Round(time.Second)
+ seconds := int(duration.Seconds())
+ cp.log.Verbosef(" %2d:%02d %s",
+ seconds/60, seconds%60, criticalPath[i].action.Description)
+ }
+ }
+}
+
+func (cp *criticalPath) Message(level MsgLevel, msg string) {}
+
+func (cp *criticalPath) Write(p []byte) (n int, err error) { return len(p), nil }
+
+func (cp *criticalPath) criticalPath() []*node {
+ var max *node
+
+ // Find the node with the longest critical path
+ for _, node := range cp.nodes {
+ if max == nil || node.cumulativeDuration > max.cumulativeDuration {
+ max = node
+ }
+ }
+
+ // Follow the critical path back to the leaf node
+ var criticalPath []*node
+ node := max
+ for node != nil {
+ criticalPath = append(criticalPath, node)
+ node = node.input
+ }
+
+ return criticalPath
+}
diff --git a/ui/status/critical_path_test.go b/ui/status/critical_path_test.go
new file mode 100644
index 0000000..965e0ad
--- /dev/null
+++ b/ui/status/critical_path_test.go
@@ -0,0 +1,166 @@
+// Copyright 2019 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 status
+
+import (
+ "reflect"
+ "testing"
+ "time"
+)
+
+type testCriticalPath struct {
+ *criticalPath
+ Counts
+
+ actions map[int]*Action
+}
+
+type testClock time.Time
+
+func (t testClock) Now() time.Time { return time.Time(t) }
+
+func (t *testCriticalPath) start(id int, startTime time.Duration, outputs, inputs []string) {
+ t.clock = testClock(time.Unix(0, 0).Add(startTime))
+ action := &Action{
+ Description: outputs[0],
+ Outputs: outputs,
+ Inputs: inputs,
+ }
+
+ t.actions[id] = action
+ t.StartAction(action, t.Counts)
+}
+
+func (t *testCriticalPath) finish(id int, endTime time.Duration) {
+ t.clock = testClock(time.Unix(0, 0).Add(endTime))
+ t.FinishAction(ActionResult{
+ Action: t.actions[id],
+ }, t.Counts)
+}
+
+func TestCriticalPath(t *testing.T) {
+ tests := []struct {
+ name string
+ msgs func(*testCriticalPath)
+ want []string
+ wantTime time.Duration
+ }{
+ {
+ name: "empty",
+ msgs: func(cp *testCriticalPath) {},
+ },
+ {
+ name: "duplicate",
+ msgs: func(cp *testCriticalPath) {
+ cp.start(0, 0, []string{"a"}, nil)
+ cp.start(1, 0, []string{"a"}, nil)
+ cp.finish(0, 1000)
+ cp.finish(0, 2000)
+ },
+ want: []string{"a"},
+ wantTime: 1000,
+ },
+ {
+ name: "linear",
+ // a
+ // |
+ // b
+ // |
+ // c
+ msgs: func(cp *testCriticalPath) {
+ cp.start(0, 0, []string{"a"}, nil)
+ cp.finish(0, 1000)
+ cp.start(1, 1000, []string{"b"}, []string{"a"})
+ cp.finish(1, 2000)
+ cp.start(2, 3000, []string{"c"}, []string{"b"})
+ cp.finish(2, 4000)
+ },
+ want: []string{"c", "b", "a"},
+ wantTime: 3000,
+ },
+ {
+ name: "diamond",
+ // a
+ // |\
+ // b c
+ // |/
+ // d
+ msgs: func(cp *testCriticalPath) {
+ cp.start(0, 0, []string{"a"}, nil)
+ cp.finish(0, 1000)
+ cp.start(1, 1000, []string{"b"}, []string{"a"})
+ cp.start(2, 1000, []string{"c"}, []string{"a"})
+ cp.finish(1, 2000)
+ cp.finish(2, 3000)
+ cp.start(3, 3000, []string{"d"}, []string{"b", "c"})
+ cp.finish(3, 4000)
+ },
+ want: []string{"d", "c", "a"},
+ wantTime: 4000,
+ },
+ {
+ name: "multiple",
+ // a d
+ // | |
+ // b e
+ // |
+ // c
+ msgs: func(cp *testCriticalPath) {
+ cp.start(0, 0, []string{"a"}, nil)
+ cp.start(3, 0, []string{"d"}, nil)
+ cp.finish(0, 1000)
+ cp.finish(3, 1000)
+ cp.start(1, 1000, []string{"b"}, []string{"a"})
+ cp.start(4, 1000, []string{"e"}, []string{"d"})
+ cp.finish(1, 2000)
+ cp.start(2, 2000, []string{"c"}, []string{"b"})
+ cp.finish(2, 3000)
+ cp.finish(4, 4000)
+
+ },
+ want: []string{"e", "d"},
+ wantTime: 4000,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ cp := &testCriticalPath{
+ criticalPath: NewCriticalPath(nil).(*criticalPath),
+ actions: make(map[int]*Action),
+ }
+
+ tt.msgs(cp)
+
+ criticalPath := cp.criticalPath.criticalPath()
+
+ var descs []string
+ for _, x := range criticalPath {
+ descs = append(descs, x.action.Description)
+ }
+
+ if !reflect.DeepEqual(descs, tt.want) {
+ t.Errorf("criticalPath.criticalPath() = %v, want %v", descs, tt.want)
+ }
+
+ var gotTime time.Duration
+ if len(criticalPath) > 0 {
+ gotTime = criticalPath[0].cumulativeDuration
+ }
+ if gotTime != tt.wantTime {
+ t.Errorf("cumulativeDuration[0].cumulativeDuration = %v, want %v", gotTime, tt.wantTime)
+ }
+ })
+ }
+}
diff --git a/ui/status/log.go b/ui/status/log.go
index 7badac7..9090f49 100644
--- a/ui/status/log.go
+++ b/ui/status/log.go
@@ -15,11 +15,17 @@
package status
import (
- "android/soong/ui/logger"
"compress/gzip"
+ "errors"
"fmt"
"io"
+ "io/ioutil"
"strings"
+
+ "github.com/golang/protobuf/proto"
+
+ "android/soong/ui/logger"
+ "android/soong/ui/status/build_error_proto"
)
type verboseLog struct {
@@ -77,8 +83,7 @@
}
type errorLog struct {
- w io.WriteCloser
-
+ w io.WriteCloser
empty bool
}
@@ -102,20 +107,17 @@
return
}
- cmd := result.Command
- if cmd == "" {
- cmd = result.Description
- }
-
if !e.empty {
fmt.Fprintf(e.w, "\n\n")
}
e.empty = false
fmt.Fprintf(e.w, "FAILED: %s\n", result.Description)
+
if len(result.Outputs) > 0 {
fmt.Fprintf(e.w, "Outputs: %s\n", strings.Join(result.Outputs, " "))
}
+
fmt.Fprintf(e.w, "Error: %s\n", result.Error)
if result.Command != "" {
fmt.Fprintf(e.w, "Command: %s\n", result.Command)
@@ -144,3 +146,55 @@
fmt.Fprint(e.w, string(p))
return len(p), nil
}
+
+type errorProtoLog struct {
+ errorProto soong_build_error_proto.BuildError
+ filename string
+ log logger.Logger
+}
+
+func NewProtoErrorLog(log logger.Logger, filename string) StatusOutput {
+ return &errorProtoLog{
+ errorProto: soong_build_error_proto.BuildError{},
+ filename: filename,
+ log: log,
+ }
+}
+
+func (e *errorProtoLog) StartAction(action *Action, counts Counts) {}
+
+func (e *errorProtoLog) FinishAction(result ActionResult, counts Counts) {
+ if result.Error == nil {
+ return
+ }
+
+ e.errorProto.ActionErrors = append(e.errorProto.ActionErrors, &soong_build_error_proto.BuildActionError{
+ Description: proto.String(result.Description),
+ Command: proto.String(result.Command),
+ Output: proto.String(result.Output),
+ Artifacts: result.Outputs,
+ Error: proto.String(result.Error.Error()),
+ })
+}
+
+func (e *errorProtoLog) Flush() {
+ data, err := proto.Marshal(&e.errorProto)
+ if err != nil {
+ e.log.Println("Failed to marshal build status proto: %v", err)
+ return
+ }
+ err = ioutil.WriteFile(e.filename, []byte(data), 0644)
+ if err != nil {
+ e.log.Println("Failed to write file %s: %v", e.errorProto, err)
+ }
+}
+
+func (e *errorProtoLog) Message(level MsgLevel, message string) {
+ if level > ErrorLvl {
+ e.errorProto.ErrorMessages = append(e.errorProto.ErrorMessages, message)
+ }
+}
+
+func (e *errorProtoLog) Write(p []byte) (int, error) {
+ return 0, errors.New("not supported")
+}
diff --git a/ui/status/ninja.go b/ui/status/ninja.go
index ee2a2da..9cf2f6a 100644
--- a/ui/status/ninja.go
+++ b/ui/status/ninja.go
@@ -142,6 +142,7 @@
action := &Action{
Description: msg.EdgeStarted.GetDesc(),
Outputs: msg.EdgeStarted.Outputs,
+ Inputs: msg.EdgeStarted.Inputs,
Command: msg.EdgeStarted.GetCommand(),
}
n.status.StartAction(action)
diff --git a/ui/status/status.go b/ui/status/status.go
index 3d8cd7a..df33baa 100644
--- a/ui/status/status.go
+++ b/ui/status/status.go
@@ -32,6 +32,10 @@
// but they can be any string.
Outputs []string
+ // Inputs is the (optional) list of inputs. Usually these are files,
+ // but they can be any string.
+ Inputs []string
+
// Command is the actual command line executed to perform the action.
// It's optional, but one of either Description or Command should be
// set.
diff --git a/ui/terminal/smart_status.go b/ui/terminal/smart_status.go
index 82c04d4..8659d4d 100644
--- a/ui/terminal/smart_status.go
+++ b/ui/terminal/smart_status.go
@@ -19,13 +19,22 @@
"io"
"os"
"os/signal"
+ "strconv"
"strings"
"sync"
"syscall"
+ "time"
"android/soong/ui/status"
)
+const tableHeightEnVar = "SOONG_UI_TABLE_HEIGHT"
+
+type actionTableEntry struct {
+ action *status.Action
+ startTime time.Time
+}
+
type smartStatusOutput struct {
writer io.Writer
formatter formatter
@@ -34,7 +43,14 @@
haveBlankLine bool
- termWidth int
+ tableMode bool
+ tableHeight int
+ requestedTableHeight int
+ termWidth, termHeight int
+
+ runningActions []actionTableEntry
+ ticker *time.Ticker
+ done chan bool
sigwinch chan os.Signal
sigwinchHandled chan bool
}
@@ -43,17 +59,41 @@
// current build status similarly to Ninja's built-in terminal
// output.
func NewSmartStatusOutput(w io.Writer, formatter formatter) status.StatusOutput {
+ tableHeight, _ := strconv.Atoi(os.Getenv(tableHeightEnVar))
+
s := &smartStatusOutput{
writer: w,
formatter: formatter,
haveBlankLine: true,
+ tableMode: tableHeight > 0,
+ requestedTableHeight: tableHeight,
+
+ done: make(chan bool),
sigwinch: make(chan os.Signal),
}
s.updateTermSize()
+ if s.tableMode {
+ // Add empty lines at the bottom of the screen to scroll back the existing history
+ // and make room for the action table.
+ // TODO: read the cursor position to see if the empty lines are necessary?
+ for i := 0; i < s.tableHeight; i++ {
+ fmt.Fprintln(w)
+ }
+
+ // Hide the cursor to prevent seeing it bouncing around
+ fmt.Fprintf(s.writer, ansi.hideCursor())
+
+ // Configure the empty action table
+ s.actionTable()
+
+ // Start a tick to update the action table periodically
+ s.startActionTableTick()
+ }
+
s.startSigwinch()
return s
@@ -77,6 +117,8 @@
}
func (s *smartStatusOutput) StartAction(action *status.Action, counts status.Counts) {
+ startTime := time.Now()
+
str := action.Description
if str == "" {
str = action.Command
@@ -87,6 +129,11 @@
s.lock.Lock()
defer s.lock.Unlock()
+ s.runningActions = append(s.runningActions, actionTableEntry{
+ action: action,
+ startTime: startTime,
+ })
+
s.statusLine(progress + str)
}
@@ -103,6 +150,13 @@
s.lock.Lock()
defer s.lock.Unlock()
+ for i, runningAction := range s.runningActions {
+ if runningAction.action == result.Action {
+ s.runningActions = append(s.runningActions[:i], s.runningActions[i+1:]...)
+ break
+ }
+ }
+
if output != "" {
s.statusLine(progress)
s.requestLine()
@@ -119,6 +173,23 @@
s.stopSigwinch()
s.requestLine()
+
+ s.runningActions = nil
+
+ if s.tableMode {
+ s.stopActionTableTick()
+
+ // Update the table after clearing runningActions to clear it
+ s.actionTable()
+
+ // Reset the scrolling region to the whole terminal
+ fmt.Fprintf(s.writer, ansi.resetScrollingMargins())
+ _, height, _ := termSize(s.writer)
+ // Move the cursor to the top of the now-blank, previously non-scrolling region
+ fmt.Fprintf(s.writer, ansi.setCursor(height-s.tableHeight, 0))
+ // Turn the cursor back on
+ fmt.Fprintf(s.writer, ansi.showCursor())
+ }
}
func (s *smartStatusOutput) Write(p []byte) (int, error) {
@@ -137,7 +208,7 @@
func (s *smartStatusOutput) print(str string) {
if !s.haveBlankLine {
- fmt.Fprint(s.writer, "\r", "\x1b[K")
+ fmt.Fprint(s.writer, "\r", ansi.clearToEndOfLine())
s.haveBlankLine = true
}
fmt.Fprint(s.writer, str)
@@ -154,34 +225,56 @@
// Limit line width to the terminal width, otherwise we'll wrap onto
// another line and we won't delete the previous line.
- if s.termWidth > 0 {
- str = s.elide(str)
- }
+ str = elide(str, s.termWidth)
// Move to the beginning on the line, turn on bold, print the output,
// turn off bold, then clear the rest of the line.
- start := "\r\x1b[1m"
- end := "\x1b[0m\x1b[K"
+ start := "\r" + ansi.bold()
+ end := ansi.regular() + ansi.clearToEndOfLine()
fmt.Fprint(s.writer, start, str, end)
s.haveBlankLine = false
}
-func (s *smartStatusOutput) elide(str string) string {
- if len(str) > s.termWidth {
+func elide(str string, width int) string {
+ if width > 0 && len(str) > width {
// TODO: Just do a max. Ninja elides the middle, but that's
// more complicated and these lines aren't that important.
- str = str[:s.termWidth]
+ str = str[:width]
}
return str
}
+func (s *smartStatusOutput) startActionTableTick() {
+ s.ticker = time.NewTicker(time.Second)
+ go func() {
+ for {
+ select {
+ case <-s.ticker.C:
+ s.lock.Lock()
+ s.actionTable()
+ s.lock.Unlock()
+ case <-s.done:
+ return
+ }
+ }
+ }()
+}
+
+func (s *smartStatusOutput) stopActionTableTick() {
+ s.ticker.Stop()
+ s.done <- true
+}
+
func (s *smartStatusOutput) startSigwinch() {
signal.Notify(s.sigwinch, syscall.SIGWINCH)
go func() {
for _ = range s.sigwinch {
s.lock.Lock()
s.updateTermSize()
+ if s.tableMode {
+ s.actionTable()
+ }
s.lock.Unlock()
if s.sigwinchHandled != nil {
s.sigwinchHandled <- true
@@ -196,7 +289,139 @@
}
func (s *smartStatusOutput) updateTermSize() {
- if w, ok := termWidth(s.writer); ok {
- s.termWidth = w
+ if w, h, ok := termSize(s.writer); ok {
+ firstUpdate := s.termHeight == 0 && s.termWidth == 0
+ oldScrollingHeight := s.termHeight - s.tableHeight
+
+ s.termWidth, s.termHeight = w, h
+
+ if s.tableMode {
+ tableHeight := s.requestedTableHeight
+ if tableHeight > s.termHeight-1 {
+ tableHeight = s.termHeight - 1
+ }
+ s.tableHeight = tableHeight
+
+ scrollingHeight := s.termHeight - s.tableHeight
+
+ if !firstUpdate {
+ // If the scrolling region has changed, attempt to pan the existing text so that it is
+ // not overwritten by the table.
+ if scrollingHeight < oldScrollingHeight {
+ pan := oldScrollingHeight - scrollingHeight
+ if pan > s.tableHeight {
+ pan = s.tableHeight
+ }
+ fmt.Fprint(s.writer, ansi.panDown(pan))
+ }
+ }
+ }
}
}
+
+func (s *smartStatusOutput) actionTable() {
+ scrollingHeight := s.termHeight - s.tableHeight
+
+ // Update the scrolling region in case the height of the terminal changed
+ fmt.Fprint(s.writer, ansi.setScrollingMargins(0, scrollingHeight))
+ // Move the cursor to the first line of the non-scrolling region
+ fmt.Fprint(s.writer, ansi.setCursor(scrollingHeight+1, 0))
+
+ // Write as many status lines as fit in the table
+ var tableLine int
+ var runningAction actionTableEntry
+ for tableLine, runningAction = range s.runningActions {
+ if tableLine >= s.tableHeight {
+ break
+ }
+
+ seconds := int(time.Since(runningAction.startTime).Round(time.Second).Seconds())
+
+ desc := runningAction.action.Description
+ if desc == "" {
+ desc = runningAction.action.Command
+ }
+
+ color := ""
+ if seconds >= 60 {
+ color = ansi.red() + ansi.bold()
+ } else if seconds >= 30 {
+ color = ansi.yellow() + ansi.bold()
+ }
+
+ durationStr := fmt.Sprintf(" %2d:%02d ", seconds/60, seconds%60)
+ desc = elide(desc, s.termWidth-len(durationStr))
+ durationStr = color + durationStr + ansi.regular()
+
+ fmt.Fprint(s.writer, durationStr, desc, ansi.clearToEndOfLine())
+ if tableLine < s.tableHeight-1 {
+ fmt.Fprint(s.writer, "\n")
+ }
+ }
+
+ // Clear any remaining lines in the table
+ for ; tableLine < s.tableHeight; tableLine++ {
+ fmt.Fprint(s.writer, ansi.clearToEndOfLine())
+ if tableLine < s.tableHeight-1 {
+ fmt.Fprint(s.writer, "\n")
+ }
+ }
+
+ // Move the cursor back to the last line of the scrolling region
+ fmt.Fprint(s.writer, ansi.setCursor(scrollingHeight, 0))
+}
+
+var ansi = ansiImpl{}
+
+type ansiImpl struct{}
+
+func (ansiImpl) clearToEndOfLine() string {
+ return "\x1b[K"
+}
+
+func (ansiImpl) setCursor(row, column int) string {
+ // Direct cursor address
+ return fmt.Sprintf("\x1b[%d;%dH", row, column)
+}
+
+func (ansiImpl) setScrollingMargins(top, bottom int) string {
+ // Set Top and Bottom Margins DECSTBM
+ return fmt.Sprintf("\x1b[%d;%dr", top, bottom)
+}
+
+func (ansiImpl) resetScrollingMargins() string {
+ // Set Top and Bottom Margins DECSTBM
+ return fmt.Sprintf("\x1b[r")
+}
+
+func (ansiImpl) red() string {
+ return "\x1b[31m"
+}
+
+func (ansiImpl) yellow() string {
+ return "\x1b[33m"
+}
+
+func (ansiImpl) bold() string {
+ return "\x1b[1m"
+}
+
+func (ansiImpl) regular() string {
+ return "\x1b[0m"
+}
+
+func (ansiImpl) showCursor() string {
+ return "\x1b[?25h"
+}
+
+func (ansiImpl) hideCursor() string {
+ return "\x1b[?25l"
+}
+
+func (ansiImpl) panDown(lines int) string {
+ return fmt.Sprintf("\x1b[%dS", lines)
+}
+
+func (ansiImpl) panUp(lines int) string {
+ return fmt.Sprintf("\x1b[%dT", lines)
+}
diff --git a/ui/terminal/status_test.go b/ui/terminal/status_test.go
index 106d651..81aa238 100644
--- a/ui/terminal/status_test.go
+++ b/ui/terminal/status_test.go
@@ -17,6 +17,7 @@
import (
"bytes"
"fmt"
+ "os"
"syscall"
"testing"
@@ -86,8 +87,11 @@
},
}
+ os.Setenv(tableHeightEnVar, "")
+
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
+
t.Run("smart", func(t *testing.T) {
smart := &fakeSmartTerminal{termWidth: 40}
stat := NewStatusOutput(smart, "", false)
@@ -251,6 +255,8 @@
}
func TestSmartStatusOutputWidthChange(t *testing.T) {
+ os.Setenv(tableHeightEnVar, "")
+
smart := &fakeSmartTerminal{termWidth: 40}
stat := NewStatusOutput(smart, "", false)
smartStat := stat.(*smartStatusOutput)
diff --git a/ui/terminal/util.go b/ui/terminal/util.go
index 3a11b79..f383ef1 100644
--- a/ui/terminal/util.go
+++ b/ui/terminal/util.go
@@ -23,6 +23,9 @@
)
func isSmartTerminal(w io.Writer) bool {
+ if term, ok := os.LookupEnv("TERM"); ok && term == "dumb" {
+ return false
+ }
if f, ok := w.(*os.File); ok {
var termios syscall.Termios
_, _, err := syscall.Syscall6(syscall.SYS_IOCTL, f.Fd(),
@@ -35,7 +38,7 @@
return false
}
-func termWidth(w io.Writer) (int, bool) {
+func termSize(w io.Writer) (width int, height int, ok bool) {
if f, ok := w.(*os.File); ok {
var winsize struct {
ws_row, ws_column uint16
@@ -44,11 +47,11 @@
_, _, err := syscall.Syscall6(syscall.SYS_IOCTL, f.Fd(),
syscall.TIOCGWINSZ, uintptr(unsafe.Pointer(&winsize)),
0, 0, 0)
- return int(winsize.ws_column), err == 0
+ return int(winsize.ws_column), int(winsize.ws_row), err == 0
} else if f, ok := w.(*fakeSmartTerminal); ok {
- return f.termWidth, true
+ return f.termWidth, f.termHeight, true
}
- return 0, false
+ return 0, 0, false
}
// stripAnsiEscapes strips ANSI control codes from a byte array in place.
@@ -106,5 +109,5 @@
type fakeSmartTerminal struct {
bytes.Buffer
- termWidth int
+ termWidth, termHeight int
}