blob: 5cc9c709fc95c8246f2dab3b2dac74fdbce7ff07 [file] [log] [blame]
// Copyright (C) 2019 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 selinux
import (
"fmt"
"io"
"github.com/google/blueprint"
"github.com/google/blueprint/proptools"
"android/soong/android"
"android/soong/sysprop"
)
type selinuxContextsProperties struct {
// Filenames under sepolicy directories, which will be used to generate contexts file.
Srcs []string `android:"path"`
// Output file name. Defaults to module name
Stem *string
Product_variables struct {
Address_sanitize struct {
Srcs []string `android:"path"`
}
}
// Whether the comments in generated contexts file will be removed or not.
Remove_comment *bool
// Whether the result context file is sorted with fc_sort or not.
Fc_sort *bool
// Make this module available when building for recovery
Recovery_available *bool
}
type seappProperties struct {
// Files containing neverallow rules.
Neverallow_files []string `android:"path"`
// Precompiled sepolicy binary file which will be fed to checkseapp.
Sepolicy *string `android:"path"`
}
type selinuxContextsModule struct {
android.ModuleBase
android.DefaultableModuleBase
flaggableModuleBase
properties selinuxContextsProperties
seappProperties seappProperties
build func(ctx android.ModuleContext, inputs android.Paths) android.Path
deps func(ctx android.BottomUpMutatorContext)
outputPath android.Path
installPath android.InstallPath
}
var _ flaggableModule = (*selinuxContextsModule)(nil)
var (
reuseContextsDepTag = dependencyTag{name: "reuseContexts"}
syspropLibraryDepTag = dependencyTag{name: "sysprop_library"}
)
func init() {
pctx.HostBinToolVariable("fc_sort", "fc_sort")
android.RegisterModuleType("contexts_defaults", contextsDefaultsFactory)
android.RegisterModuleType("file_contexts", fileFactory)
android.RegisterModuleType("hwservice_contexts", hwServiceFactory)
android.RegisterModuleType("property_contexts", propertyFactory)
android.RegisterModuleType("service_contexts", serviceFactory)
android.RegisterModuleType("keystore2_key_contexts", keystoreKeyFactory)
android.RegisterModuleType("seapp_contexts", seappFactory)
android.RegisterModuleType("vndservice_contexts", vndServiceFactory)
android.RegisterModuleType("file_contexts_test", fileContextsTestFactory)
android.RegisterModuleType("property_contexts_test", propertyContextsTestFactory)
android.RegisterModuleType("hwservice_contexts_test", hwserviceContextsTestFactory)
android.RegisterModuleType("service_contexts_test", serviceContextsTestFactory)
android.RegisterModuleType("vndservice_contexts_test", vndServiceContextsTestFactory)
}
func (m *selinuxContextsModule) InstallInRoot() bool {
return m.InRecovery()
}
func (m *selinuxContextsModule) InstallInRecovery() bool {
// ModuleBase.InRecovery() checks the image variant
return m.InRecovery()
}
func (m *selinuxContextsModule) onlyInRecovery() bool {
// ModuleBase.InstallInRecovery() checks commonProperties.Recovery property
return m.ModuleBase.InstallInRecovery()
}
func (m *selinuxContextsModule) DepsMutator(ctx android.BottomUpMutatorContext) {
if m.deps != nil {
m.deps(ctx)
}
if m.InRecovery() && !m.onlyInRecovery() {
ctx.AddFarVariationDependencies([]blueprint.Variation{
{Mutator: "image", Variation: android.CoreVariation},
}, reuseContextsDepTag, ctx.ModuleName())
}
}
func (m *selinuxContextsModule) propertyContextsDeps(ctx android.BottomUpMutatorContext) {
for _, lib := range sysprop.SyspropLibraries(ctx.Config()) {
ctx.AddFarVariationDependencies([]blueprint.Variation{}, syspropLibraryDepTag, lib)
}
}
func (m *selinuxContextsModule) stem() string {
return proptools.StringDefault(m.properties.Stem, m.Name())
}
func (m *selinuxContextsModule) GenerateAndroidBuildActions(ctx android.ModuleContext) {
if m.InRecovery() {
// Installing context files at the root of the recovery partition
m.installPath = android.PathForModuleInstall(ctx)
} else {
m.installPath = android.PathForModuleInstall(ctx, "etc", "selinux")
}
if m.InRecovery() && !m.onlyInRecovery() {
dep := ctx.GetDirectDepWithTag(m.Name(), reuseContextsDepTag)
if reuseDeps, ok := dep.(*selinuxContextsModule); ok {
m.outputPath = reuseDeps.outputPath
ctx.InstallFile(m.installPath, m.stem(), m.outputPath)
return
}
}
m.outputPath = m.build(ctx, android.PathsForModuleSrc(ctx, m.properties.Srcs))
ctx.InstallFile(m.installPath, m.stem(), m.outputPath)
}
func newModule() *selinuxContextsModule {
m := &selinuxContextsModule{}
m.AddProperties(
&m.properties,
&m.seappProperties,
)
initFlaggableModule(m)
android.InitAndroidArchModule(m, android.DeviceSupported, android.MultilibCommon)
android.InitDefaultableModule(m)
android.AddLoadHook(m, func(ctx android.LoadHookContext) {
m.selinuxContextsHook(ctx)
})
return m
}
type contextsDefaults struct {
android.ModuleBase
android.DefaultsModuleBase
}
// contexts_defaults provides a set of properties that can be inherited by other contexts modules.
// (file_contexts, property_contexts, seapp_contexts, etc.) A module can use the properties from a
// contexts_defaults using `defaults: ["<:default_module_name>"]`. Properties of both modules are
// erged (when possible) by prepending the default module's values to the depending module's values.
func contextsDefaultsFactory() android.Module {
m := &contextsDefaults{}
m.AddProperties(
&selinuxContextsProperties{},
&seappProperties{},
&flagsProperties{},
)
android.InitDefaultsModule(m)
return m
}
func (m *selinuxContextsModule) selinuxContextsHook(ctx android.LoadHookContext) {
// TODO: clean this up to use build/soong/android/variable.go after b/79249983
var srcs []string
for _, sanitize := range ctx.Config().SanitizeDevice() {
if sanitize == "address" {
srcs = append(srcs, m.properties.Product_variables.Address_sanitize.Srcs...)
break
}
}
m.properties.Srcs = append(m.properties.Srcs, srcs...)
}
func (m *selinuxContextsModule) AndroidMk() android.AndroidMkData {
nameSuffix := ""
if m.InRecovery() && !m.onlyInRecovery() {
nameSuffix = ".recovery"
}
return android.AndroidMkData{
Class: "ETC",
OutputFile: android.OptionalPathForPath(m.outputPath),
SubName: nameSuffix,
Extra: []android.AndroidMkExtraFunc{
func(w io.Writer, outputFile android.Path) {
fmt.Fprintln(w, "LOCAL_MODULE_PATH :=", m.installPath.String())
fmt.Fprintln(w, "LOCAL_INSTALLED_MODULE_STEM :=", m.stem())
},
},
}
}
func (m *selinuxContextsModule) ImageMutatorBegin(ctx android.BaseModuleContext) {
if proptools.Bool(m.properties.Recovery_available) && m.ModuleBase.InstallInRecovery() {
ctx.PropertyErrorf("recovery_available",
"doesn't make sense at the same time as `recovery: true`")
}
}
func (m *selinuxContextsModule) CoreVariantNeeded(ctx android.BaseModuleContext) bool {
return !m.ModuleBase.InstallInRecovery()
}
func (m *selinuxContextsModule) RamdiskVariantNeeded(ctx android.BaseModuleContext) bool {
return false
}
func (m *selinuxContextsModule) VendorRamdiskVariantNeeded(ctx android.BaseModuleContext) bool {
return false
}
func (m *selinuxContextsModule) DebugRamdiskVariantNeeded(ctx android.BaseModuleContext) bool {
return false
}
func (m *selinuxContextsModule) RecoveryVariantNeeded(ctx android.BaseModuleContext) bool {
return m.ModuleBase.InstallInRecovery() || proptools.Bool(m.properties.Recovery_available)
}
func (m *selinuxContextsModule) ExtraImageVariations(ctx android.BaseModuleContext) []string {
return nil
}
func (m *selinuxContextsModule) SetImageVariation(ctx android.BaseModuleContext, variation string, module android.Module) {
}
var _ android.ImageInterface = (*selinuxContextsModule)(nil)
func (m *selinuxContextsModule) buildGeneralContexts(ctx android.ModuleContext, inputs android.Paths) android.Path {
builtContext := pathForModuleOut(ctx, ctx.ModuleName()+"_m4out")
rule := android.NewRuleBuilder(pctx, ctx)
newlineFile := pathForModuleOut(ctx, "newline")
rule.Command().Text("echo").FlagWithOutput("> ", newlineFile)
rule.Temporary(newlineFile)
var inputsWithNewline android.Paths
for _, input := range inputs {
inputsWithNewline = append(inputsWithNewline, input, newlineFile)
}
flags := m.getBuildFlags(ctx)
rule.Command().
Tool(ctx.Config().PrebuiltBuildTool(ctx, "m4")).
Text("--fatal-warnings -s").
FlagForEachArg("-D", ctx.DeviceConfig().SepolicyM4Defs()).
Flags(flagsToM4Macros(flags)).
Inputs(inputsWithNewline).
FlagWithOutput("> ", builtContext)
if proptools.Bool(m.properties.Remove_comment) {
rule.Temporary(builtContext)
remove_comment_output := pathForModuleOut(ctx, ctx.ModuleName()+"_remove_comment")
rule.Command().
Text("sed -e 's/#.*$//' -e '/^$/d'").
Input(builtContext).
FlagWithOutput("> ", remove_comment_output)
builtContext = remove_comment_output
}
if proptools.Bool(m.properties.Fc_sort) {
rule.Temporary(builtContext)
sorted_output := pathForModuleOut(ctx, ctx.ModuleName()+"_sorted")
rule.Command().
Tool(ctx.Config().HostToolPath(ctx, "fc_sort")).
FlagWithInput("-i ", builtContext).
FlagWithOutput("-o ", sorted_output)
builtContext = sorted_output
}
ret := pathForModuleOut(ctx, m.stem())
rule.Temporary(builtContext)
rule.Command().Text("cp").Input(builtContext).Output(ret)
rule.DeleteTemporaryFiles()
rule.Build("selinux_contexts", "building contexts: "+m.Name())
return ret
}
func (m *selinuxContextsModule) buildFileContexts(ctx android.ModuleContext, inputs android.Paths) android.Path {
if m.properties.Remove_comment == nil {
m.properties.Remove_comment = proptools.BoolPtr(true)
}
return m.buildGeneralContexts(ctx, inputs)
}
func fileFactory() android.Module {
m := newModule()
m.build = m.buildFileContexts
return m
}
func (m *selinuxContextsModule) buildServiceContexts(ctx android.ModuleContext, inputs android.Paths) android.Path {
if m.properties.Remove_comment == nil {
m.properties.Remove_comment = proptools.BoolPtr(true)
}
return m.buildGeneralContexts(ctx, inputs)
}
func (m *selinuxContextsModule) checkVendorPropertyNamespace(ctx android.ModuleContext, input android.Path) android.Path {
shippingApiLevel := ctx.DeviceConfig().ShippingApiLevel()
ApiLevelR := android.ApiLevelOrPanic(ctx, "R")
rule := android.NewRuleBuilder(pctx, ctx)
// This list is from vts_treble_sys_prop_test.
allowedPropertyPrefixes := []string{
"ctl.odm.",
"ctl.vendor.",
"ctl.start$odm.",
"ctl.start$vendor.",
"ctl.stop$odm.",
"ctl.stop$vendor.",
"init.svc.odm.",
"init.svc.vendor.",
"ro.boot.",
"ro.hardware.",
"ro.odm.",
"ro.vendor.",
"odm.",
"persist.odm.",
"persist.vendor.",
"vendor.",
}
// persist.camera is also allowed for devices launching with R or eariler
if shippingApiLevel.LessThanOrEqualTo(ApiLevelR) {
allowedPropertyPrefixes = append(allowedPropertyPrefixes, "persist.camera.")
}
var allowedContextPrefixes []string
if shippingApiLevel.GreaterThanOrEqualTo(ApiLevelR) {
// This list is from vts_treble_sys_prop_test.
allowedContextPrefixes = []string{
"vendor_",
"odm_",
}
}
cmd := rule.Command().
BuiltTool("check_prop_prefix").
FlagWithInput("--property-contexts ", input).
FlagForEachArg("--allowed-property-prefix ", proptools.ShellEscapeList(allowedPropertyPrefixes)). // contains shell special character '$'
FlagForEachArg("--allowed-context-prefix ", allowedContextPrefixes)
if !ctx.DeviceConfig().BuildBrokenVendorPropertyNamespace() {
cmd.Flag("--strict")
}
out := pathForModuleOut(ctx, ctx.ModuleName()+"_namespace_checked")
rule.Command().Text("cp -f").Input(input).Output(out)
rule.Build("check_namespace", "checking namespace of "+ctx.ModuleName())
return out
}
func (m *selinuxContextsModule) buildPropertyContexts(ctx android.ModuleContext, inputs android.Paths) android.Path {
// vendor/odm properties are enforced for devices launching with Android Q or later. So, if
// vendor/odm, make sure that only vendor/odm properties exist.
builtCtxFile := m.buildGeneralContexts(ctx, inputs)
shippingApiLevel := ctx.DeviceConfig().ShippingApiLevel()
ApiLevelQ := android.ApiLevelOrPanic(ctx, "Q")
if (ctx.SocSpecific() || ctx.DeviceSpecific()) && shippingApiLevel.GreaterThanOrEqualTo(ApiLevelQ) {
builtCtxFile = m.checkVendorPropertyNamespace(ctx, builtCtxFile)
}
var apiFiles android.Paths
ctx.VisitDirectDepsWithTag(syspropLibraryDepTag, func(c android.Module) {
i, ok := c.(interface{ CurrentSyspropApiFile() android.OptionalPath })
if !ok {
panic(fmt.Errorf("unknown dependency %q for %q", ctx.OtherModuleName(c), ctx.ModuleName()))
}
if api := i.CurrentSyspropApiFile(); api.Valid() {
apiFiles = append(apiFiles, api.Path())
}
})
// check compatibility with sysprop_library
if len(apiFiles) > 0 {
out := pathForModuleOut(ctx, ctx.ModuleName()+"_api_checked")
rule := android.NewRuleBuilder(pctx, ctx)
msg := `\n******************************\n` +
`API of sysprop_library doesn't match with property_contexts\n` +
`Please fix the breakage and rebuild.\n` +
`******************************\n`
rule.Command().
Text("( ").
BuiltTool("sysprop_type_checker").
FlagForEachInput("--api ", apiFiles).
FlagWithInput("--context ", builtCtxFile).
Text(" || ( echo").Flag("-e").
Flag(`"` + msg + `"`).
Text("; exit 38) )")
rule.Command().Text("cp -f").Input(builtCtxFile).Output(out)
rule.Build("property_contexts_check_api", "checking API: "+m.Name())
builtCtxFile = out
}
return builtCtxFile
}
func (m *selinuxContextsModule) shouldCheckCoredomain(ctx android.ModuleContext) bool {
if !ctx.SocSpecific() && !ctx.DeviceSpecific() {
return false
}
return ctx.DeviceConfig().CheckVendorSeappViolations()
}
func (m *selinuxContextsModule) buildSeappContexts(ctx android.ModuleContext, inputs android.Paths) android.Path {
neverallowFile := pathForModuleOut(ctx, "neverallow")
ret := pathForModuleOut(ctx, "checkseapp", m.stem())
// Step 1. Generate a M4 processed neverallow file
flags := m.getBuildFlags(ctx)
m4NeverallowFile := pathForModuleOut(ctx, "neverallow.m4out")
rule := android.NewRuleBuilder(pctx, ctx)
rule.Command().
Tool(ctx.Config().PrebuiltBuildTool(ctx, "m4")).
Flag("--fatal-warnings").
FlagForEachArg("-D", ctx.DeviceConfig().SepolicyM4Defs()).
Flags(flagsToM4Macros(flags)).
Inputs(android.PathsForModuleSrc(ctx, m.seappProperties.Neverallow_files)).
FlagWithOutput("> ", m4NeverallowFile)
rule.Temporary(m4NeverallowFile)
rule.Command().
Text("( grep").
Flag("-ihe").
Text("'^neverallow'").
Input(m4NeverallowFile).
Text(">").
Output(neverallowFile).
Text("|| true )") // to make ninja happy even when result is empty
// Step 2. Generate a M4 processed contexts file
builtCtx := m.buildGeneralContexts(ctx, inputs)
// Step 3. checkseapp
rule.Temporary(neverallowFile)
checkCmd := rule.Command().BuiltTool("checkseapp").
FlagWithInput("-p ", android.PathForModuleSrc(ctx, proptools.String(m.seappProperties.Sepolicy))).
FlagWithOutput("-o ", ret).
Input(builtCtx).
Input(neverallowFile)
if m.shouldCheckCoredomain(ctx) {
checkCmd.Flag("-c") // check coredomain for vendor contexts
}
rule.Build("seapp_contexts", "Building seapp_contexts: "+m.Name())
return ret
}
func hwServiceFactory() android.Module {
m := newModule()
m.build = m.buildServiceContexts
return m
}
func propertyFactory() android.Module {
m := newModule()
m.build = m.buildPropertyContexts
m.deps = m.propertyContextsDeps
return m
}
func serviceFactory() android.Module {
m := newModule()
m.build = m.buildServiceContexts
return m
}
func keystoreKeyFactory() android.Module {
m := newModule()
m.build = m.buildGeneralContexts
return m
}
func seappFactory() android.Module {
m := newModule()
m.build = m.buildSeappContexts
return m
}
func vndServiceFactory() android.Module {
m := newModule()
m.build = m.buildGeneralContexts
android.AddLoadHook(m, func(ctx android.LoadHookContext) {
if !ctx.SocSpecific() {
ctx.ModuleErrorf(m.Name(), "must set vendor: true")
return
}
})
return m
}
var _ android.OutputFileProducer = (*selinuxContextsModule)(nil)
// Implements android.OutputFileProducer
func (m *selinuxContextsModule) OutputFiles(tag string) (android.Paths, error) {
if tag == "" {
return []android.Path{m.outputPath}, nil
}
return nil, fmt.Errorf("unsupported module reference tag %q", tag)
}
type contextsTestProperties struct {
// Contexts files to be tested.
Srcs []string `android:"path"`
// Precompiled sepolicy binary to be tesed together.
Sepolicy *string `android:"path"`
}
type fileContextsTestProperties struct {
// Test data. File passed to `checkfc -t` to validate how contexts are resolved.
Test_data *string `android:"path"`
}
type contextsTestModule struct {
android.ModuleBase
// The type of context.
context contextType
properties contextsTestProperties
fileProperties fileContextsTestProperties
testTimestamp android.OutputPath
}
type contextType int
const (
FileContext contextType = iota
PropertyContext
ServiceContext
HwServiceContext
VndServiceContext
)
// checkfc parses a context file and checks for syntax errors.
// If -s is specified, the service backend is used to verify binder services.
// If -l is specified, the service backend is used to verify hwbinder services.
// Otherwise, context_file is assumed to be a file_contexts file
// If -e is specified, then the context_file is allowed to be empty.
// file_contexts_test tests given file_contexts files with checkfc.
func fileContextsTestFactory() android.Module {
m := &contextsTestModule{context: FileContext}
m.AddProperties(&m.properties)
m.AddProperties(&m.fileProperties)
android.InitAndroidArchModule(m, android.DeviceSupported, android.MultilibCommon)
return m
}
// property_contexts_test tests given property_contexts files with property_info_checker.
func propertyContextsTestFactory() android.Module {
m := &contextsTestModule{context: PropertyContext}
m.AddProperties(&m.properties)
android.InitAndroidArchModule(m, android.DeviceSupported, android.MultilibCommon)
return m
}
// hwservice_contexts_test tests given hwservice_contexts files with checkfc.
func hwserviceContextsTestFactory() android.Module {
m := &contextsTestModule{context: HwServiceContext}
m.AddProperties(&m.properties)
android.InitAndroidArchModule(m, android.DeviceSupported, android.MultilibCommon)
return m
}
// service_contexts_test tests given service_contexts files with checkfc.
func serviceContextsTestFactory() android.Module {
// checkfc -s: service_contexts test
m := &contextsTestModule{context: ServiceContext}
m.AddProperties(&m.properties)
android.InitAndroidArchModule(m, android.DeviceSupported, android.MultilibCommon)
return m
}
// vndservice_contexts_test tests given vndservice_contexts files with checkfc.
func vndServiceContextsTestFactory() android.Module {
m := &contextsTestModule{context: VndServiceContext}
m.AddProperties(&m.properties)
android.InitAndroidArchModule(m, android.DeviceSupported, android.MultilibCommon)
return m
}
func (m *contextsTestModule) GenerateAndroidBuildActions(ctx android.ModuleContext) {
tool := "checkfc"
if m.context == PropertyContext {
tool = "property_info_checker"
}
if len(m.properties.Srcs) == 0 {
ctx.PropertyErrorf("srcs", "can't be empty")
return
}
validateWithPolicy := true
if proptools.String(m.properties.Sepolicy) == "" {
if m.context == FileContext {
if proptools.String(m.fileProperties.Test_data) == "" {
ctx.PropertyErrorf("test_data", "Either test_data or sepolicy should be provided")
return
}
validateWithPolicy = false
} else {
ctx.PropertyErrorf("sepolicy", "can't be empty")
return
}
}
flags := []string(nil)
switch m.context {
case FileContext:
if !validateWithPolicy {
flags = []string{"-t"}
}
case ServiceContext:
flags = []string{"-s" /* binder services */}
case HwServiceContext:
flags = []string{"-e" /* allow empty */, "-l" /* hwbinder services */}
case VndServiceContext:
flags = []string{"-e" /* allow empty */, "-v" /* vnd service */}
}
srcs := android.PathsForModuleSrc(ctx, m.properties.Srcs)
rule := android.NewRuleBuilder(pctx, ctx)
if validateWithPolicy {
sepolicy := android.PathForModuleSrc(ctx, proptools.String(m.properties.Sepolicy))
rule.Command().BuiltTool(tool).
Flags(flags).
Input(sepolicy).
Inputs(srcs)
} else {
test_data := android.PathForModuleSrc(ctx, proptools.String(m.fileProperties.Test_data))
rule.Command().BuiltTool(tool).
Flags(flags).
Inputs(srcs).
Input(test_data)
}
m.testTimestamp = pathForModuleOut(ctx, "timestamp")
rule.Command().Text("touch").Output(m.testTimestamp)
rule.Build("contexts_test", "running contexts test: "+ctx.ModuleName())
}
func (m *contextsTestModule) AndroidMkEntries() []android.AndroidMkEntries {
return []android.AndroidMkEntries{android.AndroidMkEntries{
Class: "FAKE",
// OutputFile is needed, even though BUILD_PHONY_PACKAGE doesn't use it.
// Without OutputFile this module won't be exported to Makefile.
OutputFile: android.OptionalPathForPath(m.testTimestamp),
Include: "$(BUILD_PHONY_PACKAGE)",
ExtraEntries: []android.AndroidMkExtraEntriesFunc{
func(ctx android.AndroidMkExtraEntriesContext, entries *android.AndroidMkEntries) {
entries.SetString("LOCAL_ADDITIONAL_DEPENDENCIES", m.testTimestamp.String())
},
},
}}
}
// contextsTestModule implements ImageInterface to be able to include recovery_available contexts
// modules as its sources.
func (m *contextsTestModule) ImageMutatorBegin(ctx android.BaseModuleContext) {
}
func (m *contextsTestModule) CoreVariantNeeded(ctx android.BaseModuleContext) bool {
return true
}
func (m *contextsTestModule) RamdiskVariantNeeded(ctx android.BaseModuleContext) bool {
return false
}
func (m *contextsTestModule) VendorRamdiskVariantNeeded(ctx android.BaseModuleContext) bool {
return false
}
func (m *contextsTestModule) DebugRamdiskVariantNeeded(ctx android.BaseModuleContext) bool {
return false
}
func (m *contextsTestModule) RecoveryVariantNeeded(ctx android.BaseModuleContext) bool {
return false
}
func (m *contextsTestModule) ExtraImageVariations(ctx android.BaseModuleContext) []string {
return nil
}
func (m *contextsTestModule) SetImageVariation(ctx android.BaseModuleContext, variation string, module android.Module) {
}
var _ android.ImageInterface = (*contextsTestModule)(nil)