diff --git a/apex/Android.bp b/apex/Android.bp index 14c877100..6269757db 100644 --- a/apex/Android.bp +++ b/apex/Android.bp @@ -31,6 +31,7 @@ bootstrap_go_package { testSrcs: [ "apex_test.go", "bootclasspath_fragment_test.go", + "classpath_element_test.go", "platform_bootclasspath_test.go", "systemserver_classpath_fragment_test.go", "vndk_test.go", diff --git a/apex/classpath_element_test.go b/apex/classpath_element_test.go new file mode 100644 index 000000000..0193127b8 --- /dev/null +++ b/apex/classpath_element_test.go @@ -0,0 +1,317 @@ +// Copyright (C) 2021 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package apex + +import ( + "reflect" + "testing" + + "android/soong/android" + "android/soong/java" + "github.com/google/blueprint" +) + +// Contains tests for java.CreateClasspathElements logic from java/classpath_element.go that +// requires apexes. + +// testClasspathElementContext is a ClasspathElementContext suitable for use in tests. +type testClasspathElementContext struct { + testContext *android.TestContext + module android.Module + errs []error +} + +func (t *testClasspathElementContext) OtherModuleHasProvider(module blueprint.Module, provider blueprint.ProviderKey) bool { + return t.testContext.ModuleHasProvider(module, provider) +} + +func (t *testClasspathElementContext) OtherModuleProvider(module blueprint.Module, provider blueprint.ProviderKey) interface{} { + return t.testContext.ModuleProvider(module, provider) +} + +func (t *testClasspathElementContext) ModuleErrorf(fmt string, args ...interface{}) { + t.errs = append(t.errs, t.testContext.ModuleErrorf(t.module, fmt, args...)) +} + +var _ java.ClasspathElementContext = (*testClasspathElementContext)(nil) + +func TestCreateClasspathElements(t *testing.T) { + preparer := android.GroupFixturePreparers( + prepareForTestWithPlatformBootclasspath, + prepareForTestWithArtApex, + prepareForTestWithMyapex, + // For otherapex. + android.FixtureMergeMockFs(android.MockFS{ + "system/sepolicy/apex/otherapex-file_contexts": nil, + }), + java.PrepareForTestWithJavaSdkLibraryFiles, + java.FixtureWithLastReleaseApis("foo", "othersdklibrary"), + android.FixtureWithRootAndroidBp(` + apex { + name: "com.android.art", + key: "com.android.art.key", + bootclasspath_fragments: [ + "art-bootclasspath-fragment", + ], + java_libs: [ + "othersdklibrary", + ], + updatable: false, + } + + apex_key { + name: "com.android.art.key", + public_key: "com.android.art.avbpubkey", + private_key: "com.android.art.pem", + } + + bootclasspath_fragment { + name: "art-bootclasspath-fragment", + apex_available: [ + "com.android.art", + ], + contents: [ + "baz", + "quuz", + ], + } + + java_library { + name: "baz", + apex_available: [ + "com.android.art", + ], + srcs: ["b.java"], + installable: true, + } + + java_library { + name: "quuz", + apex_available: [ + "com.android.art", + ], + srcs: ["b.java"], + installable: true, + } + + apex { + name: "myapex", + key: "myapex.key", + bootclasspath_fragments: [ + "mybootclasspath-fragment", + ], + java_libs: [ + "othersdklibrary", + ], + updatable: false, + } + + apex_key { + name: "myapex.key", + public_key: "testkey.avbpubkey", + private_key: "testkey.pem", + } + + bootclasspath_fragment { + name: "mybootclasspath-fragment", + apex_available: [ + "myapex", + ], + contents: [ + "bar", + ], + } + + java_library { + name: "bar", + srcs: ["b.java"], + installable: true, + apex_available: ["myapex"], + permitted_packages: ["bar"], + } + + java_sdk_library { + name: "foo", + srcs: ["b.java"], + } + + java_sdk_library { + name: "othersdklibrary", + srcs: ["b.java"], + shared_library: false, + apex_available: [ + "com.android.art", + "myapex", + ], + } + + bootclasspath_fragment { + name: "non-apex-fragment", + contents: ["othersdklibrary"], + } + + apex { + name: "otherapex", + key: "otherapex.key", + java_libs: [ + "otherapexlibrary", + ], + updatable: false, + } + + apex_key { + name: "otherapex.key", + public_key: "testkey.avbpubkey", + private_key: "testkey.pem", + } + + java_library { + name: "otherapexlibrary", + srcs: ["b.java"], + installable: true, + apex_available: ["otherapex"], + permitted_packages: ["otherapexlibrary"], + } + + platform_bootclasspath { + name: "myplatform-bootclasspath", + + fragments: [ + { + apex: "com.android.art", + module: "art-bootclasspath-fragment", + }, + ], + } + `), + ) + + result := preparer.RunTest(t) + + artFragment := result.Module("art-bootclasspath-fragment", "android_common_apex10000") + artBaz := result.Module("baz", "android_common_apex10000") + artQuuz := result.Module("quuz", "android_common_apex10000") + + myFragment := result.Module("mybootclasspath-fragment", "android_common_apex10000") + myBar := result.Module("bar", "android_common_apex10000") + + nonApexFragment := result.Module("non-apex-fragment", "android_common") + other := result.Module("othersdklibrary", "android_common_apex10000") + + otherApexLibrary := result.Module("otherapexlibrary", "android_common_apex10000") + + platformFoo := result.Module("quuz", "android_common") + + bootclasspath := result.Module("myplatform-bootclasspath", "android_common") + + // Use a custom assertion method instead of AssertDeepEquals as the latter formats the output + // using %#v which results in meaningless output as ClasspathElements are pointers. + assertElementsEquals := func(t *testing.T, message string, expected, actual java.ClasspathElements) { + if !reflect.DeepEqual(expected, actual) { + t.Errorf("%s: expected:\n %s\n got:\n %s", message, expected, actual) + } + } + + expectFragmentElement := func(module android.Module, contents ...android.Module) java.ClasspathElement { + return &java.ClasspathFragmentElement{module, contents} + } + expectLibraryElement := func(module android.Module) java.ClasspathElement { + return &java.ClasspathLibraryElement{module} + } + + newCtx := func() *testClasspathElementContext { + return &testClasspathElementContext{testContext: result.TestContext, module: bootclasspath} + } + + // Verify that CreateClasspathElements works when given valid input. + t.Run("art:baz, art:quuz, my:bar, foo", func(t *testing.T) { + ctx := newCtx() + elements := java.CreateClasspathElements(ctx, []android.Module{artBaz, artQuuz, myBar, platformFoo}, []android.Module{artFragment, myFragment}) + expectedElements := java.ClasspathElements{ + expectFragmentElement(artFragment, artBaz, artQuuz), + expectFragmentElement(myFragment, myBar), + expectLibraryElement(platformFoo), + } + assertElementsEquals(t, "elements", expectedElements, elements) + }) + + // Verify that CreateClasspathElements detects when a fragment does not have an associated apex. + t.Run("non apex fragment", func(t *testing.T) { + ctx := newCtx() + elements := java.CreateClasspathElements(ctx, []android.Module{}, []android.Module{nonApexFragment}) + android.FailIfNoMatchingErrors(t, "fragment non-apex-fragment{.*} is not part of an apex", ctx.errs) + expectedElements := java.ClasspathElements{} + assertElementsEquals(t, "elements", expectedElements, elements) + }) + + // Verify that CreateClasspathElements detects when an apex has multiple fragments. + t.Run("multiple fragments for same apex", func(t *testing.T) { + ctx := newCtx() + elements := java.CreateClasspathElements(ctx, []android.Module{}, []android.Module{artFragment, artFragment}) + android.FailIfNoMatchingErrors(t, "apex com.android.art has multiple fragments, art-bootclasspath-fragment{.*} and art-bootclasspath-fragment{.*}", ctx.errs) + expectedElements := java.ClasspathElements{} + assertElementsEquals(t, "elements", expectedElements, elements) + }) + + // Verify that CreateClasspathElements detects when a library is in multiple fragments. + t.Run("library from multiple fragments", func(t *testing.T) { + ctx := newCtx() + elements := java.CreateClasspathElements(ctx, []android.Module{other}, []android.Module{artFragment, myFragment}) + android.FailIfNoMatchingErrors(t, "library othersdklibrary{.*} is in two separate fragments, art-bootclasspath-fragment{.*} and mybootclasspath-fragment{.*}", ctx.errs) + expectedElements := java.ClasspathElements{} + assertElementsEquals(t, "elements", expectedElements, elements) + }) + + // Verify that CreateClasspathElements detects when a fragment's contents are not contiguous and + // are separated by a library from another fragment. + t.Run("discontiguous separated by fragment", func(t *testing.T) { + ctx := newCtx() + elements := java.CreateClasspathElements(ctx, []android.Module{artBaz, myBar, artQuuz, platformFoo}, []android.Module{artFragment, myFragment}) + expectedElements := java.ClasspathElements{ + expectFragmentElement(artFragment, artBaz, artQuuz), + expectFragmentElement(myFragment, myBar), + expectLibraryElement(platformFoo), + } + assertElementsEquals(t, "elements", expectedElements, elements) + android.FailIfNoMatchingErrors(t, "libraries from the same fragment must be contiguous, however baz{.*} and quuz{os:android,arch:common,apex:apex10000} from fragment art-bootclasspath-fragment{.*} are separated by libraries from fragment mybootclasspath-fragment{.*} like bar{.*}", ctx.errs) + }) + + // Verify that CreateClasspathElements detects when a fragment's contents are not contiguous and + // are separated by a standalone library. + t.Run("discontiguous separated by library", func(t *testing.T) { + ctx := newCtx() + elements := java.CreateClasspathElements(ctx, []android.Module{artBaz, platformFoo, artQuuz, myBar}, []android.Module{artFragment, myFragment}) + expectedElements := java.ClasspathElements{ + expectFragmentElement(artFragment, artBaz, artQuuz), + expectLibraryElement(platformFoo), + expectFragmentElement(myFragment, myBar), + } + assertElementsEquals(t, "elements", expectedElements, elements) + android.FailIfNoMatchingErrors(t, "libraries from the same fragment must be contiguous, however baz{.*} and quuz{os:android,arch:common,apex:apex10000} from fragment art-bootclasspath-fragment{.*} are separated by library quuz{.*}", ctx.errs) + }) + + // Verify that CreateClasspathElements detects when there a library on the classpath that + // indicates it is from an apex the supplied fragments list does not contain a fragment for that + // apex. + t.Run("no fragment for apex", func(t *testing.T) { + ctx := newCtx() + elements := java.CreateClasspathElements(ctx, []android.Module{artBaz, otherApexLibrary}, []android.Module{artFragment}) + expectedElements := java.ClasspathElements{ + expectFragmentElement(artFragment, artBaz), + } + assertElementsEquals(t, "elements", expectedElements, elements) + android.FailIfNoMatchingErrors(t, `library otherapexlibrary{.*} is from apexes \[otherapex\] which have no corresponding fragment in \[art-bootclasspath-fragment{.*}\]`, ctx.errs) + }) +} diff --git a/java/Android.bp b/java/Android.bp index 680f3a17c..59526024a 100644 --- a/java/Android.bp +++ b/java/Android.bp @@ -33,6 +33,7 @@ bootstrap_go_package { "bootclasspath.go", "bootclasspath_fragment.go", "builder.go", + "classpath_element.go", "classpath_fragment.go", "device_host_converter.go", "dex.go", diff --git a/java/classpath_element.go b/java/classpath_element.go new file mode 100644 index 000000000..753e7f888 --- /dev/null +++ b/java/classpath_element.go @@ -0,0 +1,229 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package java + +import ( + "fmt" + "strings" + + "android/soong/android" + "github.com/google/blueprint" +) + +// Supports constructing a list of ClasspathElement from a set of fragments and modules. + +// ClasspathElement represents a component that contributes to a classpath. That can be +// either a java module or a classpath fragment module. +type ClasspathElement interface { + Module() android.Module + String() string +} + +type ClasspathElements []ClasspathElement + +// ClasspathFragmentElement is a ClasspathElement that encapsulates a classpath fragment module. +type ClasspathFragmentElement struct { + Fragment android.Module + Contents []android.Module +} + +func (b *ClasspathFragmentElement) Module() android.Module { + return b.Fragment +} + +func (b *ClasspathFragmentElement) String() string { + contents := []string{} + for _, module := range b.Contents { + contents = append(contents, module.String()) + } + return fmt.Sprintf("fragment(%s, %s)", b.Fragment, strings.Join(contents, ", ")) +} + +var _ ClasspathElement = (*ClasspathFragmentElement)(nil) + +// ClasspathLibraryElement is a ClasspathElement that encapsulates a java library. +type ClasspathLibraryElement struct { + Library android.Module +} + +func (b *ClasspathLibraryElement) Module() android.Module { + return b.Library +} + +func (b *ClasspathLibraryElement) String() string { + return fmt.Sprintf("library{%s}", b.Library) +} + +var _ ClasspathElement = (*ClasspathLibraryElement)(nil) + +// ClasspathElementContext defines the context methods needed by CreateClasspathElements +type ClasspathElementContext interface { + OtherModuleHasProvider(m blueprint.Module, provider blueprint.ProviderKey) bool + OtherModuleProvider(m blueprint.Module, provider blueprint.ProviderKey) interface{} + ModuleErrorf(fmt string, args ...interface{}) +} + +// CreateClasspathElements creates a list of ClasspathElement objects from a list of libraries and +// a list of fragments. +// +// The libraries parameter contains the set of libraries from which the classpath is constructed. +// The fragments parameter contains the classpath fragment modules whose contents are libraries that +// are part of the classpath. Each library in the libraries parameter may be part of a fragment. The +// determination as to which libraries belong to fragments and which do not is based on the apex to +// which they belong, if any. +// +// Every fragment in the fragments list must be part of one or more apexes and each apex is assumed +// to contain only a single fragment from the fragments list. A library in the libraries parameter +// that is part of an apex must be provided by a classpath fragment in the corresponding apex. +// +// This will return a ClasspathElements list that contains a ClasspathElement for each standalone +// library and each fragment. The order of the elements in the list is such that if the list was +// flattened into a list of library modules that it would result in the same list or modules as the +// input libraries. Flattening the list can be done by replacing each ClasspathFragmentElement in +// the list with its Contents field. +// +// Requirements/Assumptions: +// * A fragment can be associated with more than one apex but each apex must only be associated with +// a single fragment from the fragments list. +// * All of a fragment's contents must appear as a contiguous block in the same order in the +// libraries list. +// * Each library must only appear in a single fragment. +// +// The apex is used to identify which libraries belong to which fragment. First a mapping is created +// from apex to fragment. Then the libraries are iterated over and any library in an apex is +// associated with an element for the fragment to which it belongs. Otherwise, the libraries are +// standalone and have their own element. +// +// e.g. Given the following input: +// libraries: com.android.art:core-oj, com.android.art:core-libart, framework, ext +// fragments: com.android.art:art-bootclasspath-fragment +// +// Then this will return: +// ClasspathFragmentElement(art-bootclasspath-fragment, [core-oj, core-libart]), +// ClasspathLibraryElement(framework), +// ClasspathLibraryElement(ext), +func CreateClasspathElements(ctx ClasspathElementContext, libraries []android.Module, fragments []android.Module) ClasspathElements { + // Create a map from apex name to the fragment module. This makes it easy to find the fragment + // associated with a particular apex. + apexToFragment := map[string]android.Module{} + for _, fragment := range fragments { + if !ctx.OtherModuleHasProvider(fragment, android.ApexInfoProvider) { + ctx.ModuleErrorf("fragment %s is not part of an apex", fragment) + continue + } + + apexInfo := ctx.OtherModuleProvider(fragment, android.ApexInfoProvider).(android.ApexInfo) + for _, apex := range apexInfo.InApexVariants { + if existing, ok := apexToFragment[apex]; ok { + ctx.ModuleErrorf("apex %s has multiple fragments, %s and %s", apex, fragment, existing) + continue + } + apexToFragment[apex] = fragment + } + } + + fragmentToElement := map[android.Module]*ClasspathFragmentElement{} + elements := []ClasspathElement{} + var currentElement ClasspathElement + +skipLibrary: + // Iterate over the libraries to construct the ClasspathElements list. + for _, library := range libraries { + var element ClasspathElement + if ctx.OtherModuleHasProvider(library, android.ApexInfoProvider) { + apexInfo := ctx.OtherModuleProvider(library, android.ApexInfoProvider).(android.ApexInfo) + + var fragment android.Module + + // Make sure that the library is in only one fragment of the classpath. + for _, apex := range apexInfo.InApexVariants { + if f, ok := apexToFragment[apex]; ok { + if fragment == nil { + // This is the first fragment so just save it away. + fragment = f + } else if f != fragment { + // This apex variant of the library is in a different fragment. + ctx.ModuleErrorf("library %s is in two separate fragments, %s and %s", library, fragment, f) + // Skip over this library entirely as otherwise the resulting classpath elements would + // be invalid. + continue skipLibrary + } + } else { + // There is no fragment associated with the library's apex. + } + } + + if fragment == nil { + ctx.ModuleErrorf("library %s is from apexes %s which have no corresponding fragment in %s", + library, apexInfo.InApexVariants, fragments) + // Skip over this library entirely as otherwise the resulting classpath elements would + // be invalid. + continue skipLibrary + } else if existingFragmentElement, ok := fragmentToElement[fragment]; ok { + // This library is in a fragment element that has already been added. + + // If the existing fragment element is still the current element then this library is + // contiguous with other libraries in that fragment so there is nothing more to do. + // Otherwise this library is not contiguous with other libraries in the same fragment which + // is an error. + if existingFragmentElement != currentElement { + separator := "" + if fragmentElement, ok := currentElement.(*ClasspathFragmentElement); ok { + separator = fmt.Sprintf("libraries from fragment %s like %s", fragmentElement.Fragment, fragmentElement.Contents[0]) + } else { + libraryElement := currentElement.(*ClasspathLibraryElement) + separator = fmt.Sprintf("library %s", libraryElement.Library) + } + + // Get the library that precedes this library in the fragment. That is the last library as + // this library has not yet been added. + precedingLibraryInFragment := existingFragmentElement.Contents[len(existingFragmentElement.Contents)-1] + ctx.ModuleErrorf("libraries from the same fragment must be contiguous, however %s and %s from fragment %s are separated by %s", + precedingLibraryInFragment, library, fragment, separator) + } + + // Add this library to the fragment element's contents. + existingFragmentElement.Contents = append(existingFragmentElement.Contents, library) + } else { + // This is the first library in this fragment so add a new element for the fragment, + // including the library. + fragmentElement := &ClasspathFragmentElement{ + Fragment: fragment, + Contents: []android.Module{library}, + } + + // Store it away so we can detect when attempting to create another element for the same + // fragment. + fragmentToElement[fragment] = fragmentElement + element = fragmentElement + } + } else { + // The library is from the platform so just add an element for it. + element = &ClasspathLibraryElement{Library: library} + } + + // If no element was created then it means that the library has been added to an existing + // fragment element so the list of elements and current element are unaffected. + if element != nil { + // Add the element to the list and make it the current element for the next iteration. + elements = append(elements, element) + currentElement = element + } + } + + return elements +}