Merge "Decouple addition of new sdk member types from sdk code"
am: 0558ecce51
Change-Id: Ic1da6424bffada70a0e69bb6605495f45a2f71b2
This commit is contained in:
@@ -15,6 +15,7 @@
|
||||
package android
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/google/blueprint"
|
||||
@@ -218,18 +219,24 @@ type SdkMember interface {
|
||||
// The basic implementation should look something like this, where ModuleType is
|
||||
// the name of the module type being supported.
|
||||
//
|
||||
// var ModuleTypeSdkMemberType = newModuleTypeSdkMemberType()
|
||||
//
|
||||
// func newModuleTypeSdkMemberType() android.SdkMemberType {
|
||||
// return &moduleTypeSdkMemberType{}
|
||||
// type moduleTypeSdkMemberType struct {
|
||||
// android.SdkMemberTypeBase
|
||||
// }
|
||||
//
|
||||
// type moduleTypeSdkMemberType struct {
|
||||
// func init() {
|
||||
// android.RegisterSdkMemberType(&moduleTypeSdkMemberType{
|
||||
// SdkMemberTypeBase: android.SdkMemberTypeBase{
|
||||
// PropertyName: "module_types",
|
||||
// },
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// ...methods...
|
||||
//
|
||||
type SdkMemberType interface {
|
||||
// The name of the member type property on an sdk module.
|
||||
SdkPropertyName() string
|
||||
|
||||
// Add dependencies from the SDK module to all the variants the member
|
||||
// contributes to the SDK. The exact set of variants required is determined
|
||||
// by the SDK and its properties. The dependencies must be added with the
|
||||
@@ -254,3 +261,66 @@ type SdkMemberType interface {
|
||||
// IsInstance(Module) method returned true.
|
||||
BuildSnapshot(sdkModuleContext ModuleContext, builder SnapshotBuilder, member SdkMember)
|
||||
}
|
||||
|
||||
type SdkMemberTypeBase struct {
|
||||
PropertyName string
|
||||
}
|
||||
|
||||
func (b *SdkMemberTypeBase) SdkPropertyName() string {
|
||||
return b.PropertyName
|
||||
}
|
||||
|
||||
// Encapsulates the information about registered SdkMemberTypes.
|
||||
type SdkMemberTypesRegistry struct {
|
||||
// The list of types sorted by property name.
|
||||
list []SdkMemberType
|
||||
|
||||
// The key that uniquely identifies this registry instance.
|
||||
key OnceKey
|
||||
}
|
||||
|
||||
func (r *SdkMemberTypesRegistry) RegisteredTypes() []SdkMemberType {
|
||||
return r.list
|
||||
}
|
||||
|
||||
func (r *SdkMemberTypesRegistry) UniqueOnceKey() OnceKey {
|
||||
// Use the pointer to the registry as the unique key.
|
||||
return NewCustomOnceKey(r)
|
||||
}
|
||||
|
||||
// The set of registered SdkMemberTypes.
|
||||
var SdkMemberTypes = &SdkMemberTypesRegistry{}
|
||||
|
||||
// Register an SdkMemberType object to allow them to be used in the sdk and sdk_snapshot module
|
||||
// types.
|
||||
func RegisterSdkMemberType(memberType SdkMemberType) {
|
||||
oldList := SdkMemberTypes.list
|
||||
|
||||
// Copy the slice just in case this is being read while being modified, e.g. when testing.
|
||||
list := make([]SdkMemberType, 0, len(oldList)+1)
|
||||
list = append(list, oldList...)
|
||||
list = append(list, memberType)
|
||||
|
||||
// Sort the member types by their property name to ensure that registry order has no effect
|
||||
// on behavior.
|
||||
sort.Slice(list, func(i1, i2 int) bool {
|
||||
t1 := list[i1]
|
||||
t2 := list[i2]
|
||||
|
||||
return t1.SdkPropertyName() < t2.SdkPropertyName()
|
||||
})
|
||||
|
||||
// Generate a key that identifies the slice of SdkMemberTypes by joining the property names
|
||||
// from all the SdkMemberType .
|
||||
var properties []string
|
||||
for _, t := range list {
|
||||
properties = append(properties, t.SdkPropertyName())
|
||||
}
|
||||
key := NewOnceKey(strings.Join(properties, "|"))
|
||||
|
||||
// Create a new registry so the pointer uniquely identifies the set of registered types.
|
||||
SdkMemberTypes = &SdkMemberTypesRegistry{
|
||||
list: list,
|
||||
key: key,
|
||||
}
|
||||
}
|
||||
|
@@ -24,17 +24,28 @@ import (
|
||||
|
||||
// This file contains support for using cc library modules within an sdk.
|
||||
|
||||
var SharedLibrarySdkMemberType = &librarySdkMemberType{
|
||||
prebuiltModuleType: "cc_prebuilt_library_shared",
|
||||
linkTypes: []string{"shared"},
|
||||
}
|
||||
func init() {
|
||||
// Register sdk member types.
|
||||
android.RegisterSdkMemberType(&librarySdkMemberType{
|
||||
SdkMemberTypeBase: android.SdkMemberTypeBase{
|
||||
PropertyName: "native_shared_libs",
|
||||
},
|
||||
prebuiltModuleType: "cc_prebuilt_library_shared",
|
||||
linkTypes: []string{"shared"},
|
||||
})
|
||||
|
||||
var StaticLibrarySdkMemberType = &librarySdkMemberType{
|
||||
prebuiltModuleType: "cc_prebuilt_library_static",
|
||||
linkTypes: []string{"static"},
|
||||
android.RegisterSdkMemberType(&librarySdkMemberType{
|
||||
SdkMemberTypeBase: android.SdkMemberTypeBase{
|
||||
PropertyName: "native_static_libs",
|
||||
},
|
||||
prebuiltModuleType: "cc_prebuilt_library_static",
|
||||
linkTypes: []string{"static"},
|
||||
})
|
||||
}
|
||||
|
||||
type librarySdkMemberType struct {
|
||||
android.SdkMemberTypeBase
|
||||
|
||||
prebuiltModuleType string
|
||||
|
||||
// The set of link types supported, set of "static", "shared".
|
||||
|
@@ -40,6 +40,13 @@ func init() {
|
||||
android.RegisterModuleType("droidstubs_host", DroidstubsHostFactory)
|
||||
|
||||
android.RegisterModuleType("prebuilt_stubs_sources", PrebuiltStubsSourcesFactory)
|
||||
|
||||
// Register sdk member type.
|
||||
android.RegisterSdkMemberType(&droidStubsSdkMemberType{
|
||||
SdkMemberTypeBase: android.SdkMemberTypeBase{
|
||||
PropertyName: "stubs_sources",
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -1974,9 +1981,8 @@ func PrebuiltStubsSourcesFactory() android.Module {
|
||||
return module
|
||||
}
|
||||
|
||||
var DroidStubsSdkMemberType = &droidStubsSdkMemberType{}
|
||||
|
||||
type droidStubsSdkMemberType struct {
|
||||
android.SdkMemberTypeBase
|
||||
}
|
||||
|
||||
func (mt *droidStubsSdkMemberType) AddDependencies(mctx android.BottomUpMutatorContext, dependencyTag blueprint.DependencyTag, names []string) {
|
||||
|
22
java/java.go
22
java/java.go
@@ -52,6 +52,23 @@ func init() {
|
||||
|
||||
android.RegisterSingletonType("logtags", LogtagsSingleton)
|
||||
android.RegisterSingletonType("kythe_java_extract", kytheExtractJavaFactory)
|
||||
|
||||
// Register sdk member types.
|
||||
android.RegisterSdkMemberType(&headerLibrarySdkMemberType{
|
||||
librarySdkMemberType{
|
||||
android.SdkMemberTypeBase{
|
||||
PropertyName: "java_header_libs",
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
android.RegisterSdkMemberType(&implLibrarySdkMemberType{
|
||||
librarySdkMemberType{
|
||||
android.SdkMemberTypeBase{
|
||||
PropertyName: "java_libs",
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (j *Module) checkSdkVersion(ctx android.ModuleContext) {
|
||||
@@ -1721,6 +1738,7 @@ func (j *Library) sdkSnapshotFilePathForJar() string {
|
||||
}
|
||||
|
||||
type librarySdkMemberType struct {
|
||||
android.SdkMemberTypeBase
|
||||
}
|
||||
|
||||
func (mt *librarySdkMemberType) AddDependencies(mctx android.BottomUpMutatorContext, dependencyTag blueprint.DependencyTag, names []string) {
|
||||
@@ -1764,8 +1782,6 @@ func (mt *librarySdkMemberType) buildSnapshot(
|
||||
module.AddProperty("jars", []string{snapshotRelativeJavaLibPath})
|
||||
}
|
||||
|
||||
var HeaderLibrarySdkMemberType = &headerLibrarySdkMemberType{}
|
||||
|
||||
type headerLibrarySdkMemberType struct {
|
||||
librarySdkMemberType
|
||||
}
|
||||
@@ -1781,8 +1797,6 @@ func (mt *headerLibrarySdkMemberType) BuildSnapshot(sdkModuleContext android.Mod
|
||||
})
|
||||
}
|
||||
|
||||
var ImplLibrarySdkMemberType = &implLibrarySdkMemberType{}
|
||||
|
||||
type implLibrarySdkMemberType struct {
|
||||
librarySdkMemberType
|
||||
}
|
||||
|
195
sdk/sdk.go
195
sdk/sdk.go
@@ -17,6 +17,7 @@ package sdk
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"reflect"
|
||||
"strconv"
|
||||
|
||||
"github.com/google/blueprint"
|
||||
@@ -26,8 +27,6 @@ import (
|
||||
// This package doesn't depend on the apex package, but import it to make its mutators to be
|
||||
// registered before mutators in this package. See RegisterPostDepsMutators for more details.
|
||||
_ "android/soong/apex"
|
||||
"android/soong/cc"
|
||||
"android/soong/java"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -38,20 +37,19 @@ func init() {
|
||||
android.RegisterModuleType("sdk_snapshot", SnapshotModuleFactory)
|
||||
android.PreDepsMutators(RegisterPreDepsMutators)
|
||||
android.PostDepsMutators(RegisterPostDepsMutators)
|
||||
|
||||
// Populate the dependency tags for each member list property. This needs to
|
||||
// be done here to break an initialization cycle.
|
||||
for _, memberListProperty := range sdkMemberListProperties {
|
||||
memberListProperty.dependencyTag = &sdkMemberDependencyTag{
|
||||
memberListProperty: memberListProperty,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type sdk struct {
|
||||
android.ModuleBase
|
||||
android.DefaultableModuleBase
|
||||
|
||||
// The dynamically generated information about the registered SdkMemberType
|
||||
dynamicSdkMemberTypes *dynamicSdkMemberTypes
|
||||
|
||||
// The dynamically created instance of the properties struct containing the sdk member
|
||||
// list properties, e.g. java_libs.
|
||||
dynamicMemberTypeListProperties interface{}
|
||||
|
||||
properties sdkProperties
|
||||
|
||||
snapshotFile android.OptionalPath
|
||||
@@ -61,92 +59,141 @@ type sdk struct {
|
||||
}
|
||||
|
||||
type sdkProperties struct {
|
||||
// For module types from the cc package
|
||||
|
||||
// The list of shared native libraries in this SDK
|
||||
Native_shared_libs []string
|
||||
|
||||
// The list of static native libraries in this SDK
|
||||
Native_static_libs []string
|
||||
|
||||
// For module types from the java package
|
||||
|
||||
// The list of java header libraries in this SDK
|
||||
//
|
||||
// This should be used for java libraries that are provided separately at runtime,
|
||||
// e.g. through an APEX.
|
||||
Java_header_libs []string
|
||||
|
||||
// The list of java implementation libraries in this SDK
|
||||
Java_libs []string
|
||||
|
||||
// The list of stub sources in this SDK
|
||||
Stubs_sources []string
|
||||
|
||||
Snapshot bool `blueprint:"mutated"`
|
||||
}
|
||||
|
||||
type sdkMemberDependencyTag struct {
|
||||
blueprint.BaseDependencyTag
|
||||
memberListProperty *sdkMemberListProperty
|
||||
memberType android.SdkMemberType
|
||||
}
|
||||
|
||||
// Contains information about the sdk properties that list sdk members, e.g.
|
||||
// Java_header_libs.
|
||||
type sdkMemberListProperty struct {
|
||||
// the name of the property as used in a .bp file
|
||||
name string
|
||||
|
||||
// getter for the list of member names
|
||||
getter func(properties *sdkProperties) []string
|
||||
getter func(properties interface{}) []string
|
||||
|
||||
// the type of member referenced in the list
|
||||
memberType android.SdkMemberType
|
||||
|
||||
// the dependency tag used for items in this list.
|
||||
// the dependency tag used for items in this list that can be used to determine the memberType
|
||||
// for a resolved dependency.
|
||||
dependencyTag *sdkMemberDependencyTag
|
||||
}
|
||||
|
||||
// Information about how to handle each member list property.
|
||||
func (p *sdkMemberListProperty) propertyName() string {
|
||||
return p.memberType.SdkPropertyName()
|
||||
}
|
||||
|
||||
// Cache of dynamically generated dynamicSdkMemberTypes objects. The key is the pointer
|
||||
// to a slice of SdkMemberType instances held in android.SdkMemberTypes.
|
||||
var dynamicSdkMemberTypesMap android.OncePer
|
||||
|
||||
// A dynamically generated set of member list properties and associated structure type.
|
||||
type dynamicSdkMemberTypes struct {
|
||||
// The dynamically generated structure type.
|
||||
//
|
||||
// Contains one []string exported field for each android.SdkMemberTypes. The name of the field
|
||||
// is the exported form of the value returned by SdkMemberType.SdkPropertyName().
|
||||
propertiesStructType reflect.Type
|
||||
|
||||
// Information about each of the member type specific list properties.
|
||||
memberListProperties []*sdkMemberListProperty
|
||||
}
|
||||
|
||||
func (d *dynamicSdkMemberTypes) createMemberListProperties() interface{} {
|
||||
return reflect.New(d.propertiesStructType).Interface()
|
||||
}
|
||||
|
||||
func getDynamicSdkMemberTypes(registry *android.SdkMemberTypesRegistry) *dynamicSdkMemberTypes {
|
||||
|
||||
// Get a key that uniquely identifies the registry contents.
|
||||
key := registry.UniqueOnceKey()
|
||||
|
||||
// Get the registered types.
|
||||
registeredTypes := registry.RegisteredTypes()
|
||||
|
||||
// Get the cached value, creating new instance if necessary.
|
||||
return dynamicSdkMemberTypesMap.Once(key, func() interface{} {
|
||||
return createDynamicSdkMemberTypes(registeredTypes)
|
||||
}).(*dynamicSdkMemberTypes)
|
||||
}
|
||||
|
||||
// Create the dynamicSdkMemberTypes from the list of registered member types.
|
||||
//
|
||||
// It is organized first by package and then by name within the package.
|
||||
// Packages are in alphabetical order and properties are in alphabetical order
|
||||
// within each package.
|
||||
var sdkMemberListProperties = []*sdkMemberListProperty{
|
||||
// Members from cc package.
|
||||
{
|
||||
name: "native_shared_libs",
|
||||
getter: func(properties *sdkProperties) []string { return properties.Native_shared_libs },
|
||||
memberType: cc.SharedLibrarySdkMemberType,
|
||||
},
|
||||
{
|
||||
name: "native_static_libs",
|
||||
getter: func(properties *sdkProperties) []string { return properties.Native_static_libs },
|
||||
memberType: cc.StaticLibrarySdkMemberType,
|
||||
},
|
||||
// Members from java package.
|
||||
{
|
||||
name: "java_header_libs",
|
||||
getter: func(properties *sdkProperties) []string { return properties.Java_header_libs },
|
||||
memberType: java.HeaderLibrarySdkMemberType,
|
||||
},
|
||||
{
|
||||
name: "java_libs",
|
||||
getter: func(properties *sdkProperties) []string { return properties.Java_libs },
|
||||
memberType: java.ImplLibrarySdkMemberType,
|
||||
},
|
||||
{
|
||||
name: "stubs_sources",
|
||||
getter: func(properties *sdkProperties) []string { return properties.Stubs_sources },
|
||||
memberType: java.DroidStubsSdkMemberType,
|
||||
},
|
||||
// A struct is created which contains one exported field per member type corresponding to
|
||||
// the SdkMemberType.SdkPropertyName() value.
|
||||
//
|
||||
// A list of sdkMemberListProperty instances is created, one per member type that provides:
|
||||
// * a reference to the member type.
|
||||
// * a getter for the corresponding field in the properties struct.
|
||||
// * a dependency tag that identifies the member type of a resolved dependency.
|
||||
//
|
||||
func createDynamicSdkMemberTypes(sdkMemberTypes []android.SdkMemberType) *dynamicSdkMemberTypes {
|
||||
var listProperties []*sdkMemberListProperty
|
||||
var fields []reflect.StructField
|
||||
|
||||
// Iterate over the member types creating StructField and sdkMemberListProperty objects.
|
||||
for f, memberType := range sdkMemberTypes {
|
||||
p := memberType.SdkPropertyName()
|
||||
|
||||
// Create a dynamic exported field for the member type's property.
|
||||
fields = append(fields, reflect.StructField{
|
||||
Name: proptools.FieldNameForProperty(p),
|
||||
Type: reflect.TypeOf([]string{}),
|
||||
})
|
||||
|
||||
// Copy the field index for use in the getter func as using the loop variable directly will
|
||||
// cause all funcs to use the last value.
|
||||
fieldIndex := f
|
||||
|
||||
// Create an sdkMemberListProperty for the member type.
|
||||
memberListProperty := &sdkMemberListProperty{
|
||||
getter: func(properties interface{}) []string {
|
||||
// The properties is expected to be of the following form (where
|
||||
// <Module_types> is the name of an SdkMemberType.SdkPropertyName().
|
||||
// properties *struct {<Module_types> []string, ....}
|
||||
//
|
||||
// Although it accesses the field by index the following reflection code is equivalent to:
|
||||
// *properties.<Module_types>
|
||||
//
|
||||
list := reflect.ValueOf(properties).Elem().Field(fieldIndex).Interface().([]string)
|
||||
return list
|
||||
},
|
||||
|
||||
memberType: memberType,
|
||||
|
||||
dependencyTag: &sdkMemberDependencyTag{
|
||||
memberType: memberType,
|
||||
},
|
||||
}
|
||||
|
||||
listProperties = append(listProperties, memberListProperty)
|
||||
}
|
||||
|
||||
// Create a dynamic struct from the collated fields.
|
||||
propertiesStructType := reflect.StructOf(fields)
|
||||
|
||||
return &dynamicSdkMemberTypes{
|
||||
memberListProperties: listProperties,
|
||||
propertiesStructType: propertiesStructType,
|
||||
}
|
||||
}
|
||||
|
||||
// sdk defines an SDK which is a logical group of modules (e.g. native libs, headers, java libs, etc.)
|
||||
// which Mainline modules like APEX can choose to build with.
|
||||
func ModuleFactory() android.Module {
|
||||
s := &sdk{}
|
||||
s.AddProperties(&s.properties)
|
||||
|
||||
// Get the dynamic sdk member type data for the currently registered sdk member types.
|
||||
s.dynamicSdkMemberTypes = getDynamicSdkMemberTypes(android.SdkMemberTypes)
|
||||
|
||||
// Create an instance of the dynamically created struct that contains all the
|
||||
// properties for the member type specific list properties.
|
||||
s.dynamicMemberTypeListProperties = s.dynamicSdkMemberTypes.createMemberListProperties()
|
||||
|
||||
s.AddProperties(&s.properties, s.dynamicMemberTypeListProperties)
|
||||
|
||||
android.InitAndroidMultiTargetsArchModule(s, android.HostAndDeviceSupported, android.MultilibCommon)
|
||||
android.InitDefaultableModule(s)
|
||||
android.AddLoadHook(s, func(ctx android.LoadHookContext) {
|
||||
@@ -235,9 +282,9 @@ type sdkMemberVesionedDepTag struct {
|
||||
|
||||
// Step 1: create dependencies from an SDK module to its members.
|
||||
func memberMutator(mctx android.BottomUpMutatorContext) {
|
||||
if m, ok := mctx.Module().(*sdk); ok {
|
||||
for _, memberListProperty := range sdkMemberListProperties {
|
||||
names := memberListProperty.getter(&m.properties)
|
||||
if s, ok := mctx.Module().(*sdk); ok {
|
||||
for _, memberListProperty := range s.dynamicSdkMemberTypes.memberListProperties {
|
||||
names := memberListProperty.getter(s.dynamicMemberTypeListProperties)
|
||||
tag := memberListProperty.dependencyTag
|
||||
memberListProperty.memberType.AddDependencies(mctx, tag, names)
|
||||
}
|
||||
|
@@ -105,21 +105,20 @@ func (gf *generatedFile) build(pctx android.PackageContext, ctx android.BuilderC
|
||||
// Collect all the members.
|
||||
//
|
||||
// The members are first grouped by type and then grouped by name. The order of
|
||||
// the types is the order they are referenced in sdkMemberListProperties. The
|
||||
// the types is the order they are referenced in android.SdkMemberTypes. The
|
||||
// names are in order in which the dependencies were added.
|
||||
func collectMembers(ctx android.ModuleContext) []*sdkMember {
|
||||
func (s *sdk) collectMembers(ctx android.ModuleContext) []*sdkMember {
|
||||
byType := make(map[android.SdkMemberType][]*sdkMember)
|
||||
byName := make(map[string]*sdkMember)
|
||||
|
||||
ctx.VisitDirectDeps(func(m android.Module) {
|
||||
tag := ctx.OtherModuleDependencyTag(m)
|
||||
if memberTag, ok := tag.(*sdkMemberDependencyTag); ok {
|
||||
memberListProperty := memberTag.memberListProperty
|
||||
memberType := memberListProperty.memberType
|
||||
memberType := memberTag.memberType
|
||||
|
||||
// Make sure that the resolved module is allowed in the member list property.
|
||||
if !memberType.IsInstance(m) {
|
||||
ctx.ModuleErrorf("module %q is not valid in property %s", ctx.OtherModuleName(m), memberListProperty.name)
|
||||
ctx.ModuleErrorf("module %q is not valid in property %s", ctx.OtherModuleName(m), memberType.SdkPropertyName())
|
||||
}
|
||||
|
||||
name := ctx.OtherModuleName(m)
|
||||
@@ -136,7 +135,7 @@ func collectMembers(ctx android.ModuleContext) []*sdkMember {
|
||||
})
|
||||
|
||||
var members []*sdkMember
|
||||
for _, memberListProperty := range sdkMemberListProperties {
|
||||
for _, memberListProperty := range s.dynamicSdkMemberTypes.memberListProperties {
|
||||
membersOfType := byType[memberListProperty.memberType]
|
||||
members = append(members, membersOfType...)
|
||||
}
|
||||
@@ -191,7 +190,7 @@ func (s *sdk) buildSnapshot(ctx android.ModuleContext) android.OutputPath {
|
||||
}
|
||||
s.builderForTests = builder
|
||||
|
||||
for _, member := range collectMembers(ctx) {
|
||||
for _, member := range s.collectMembers(ctx) {
|
||||
member.memberType.BuildSnapshot(ctx, builder, member)
|
||||
}
|
||||
|
||||
@@ -220,10 +219,10 @@ func (s *sdk) buildSnapshot(ctx android.ModuleContext) android.OutputPath {
|
||||
}
|
||||
|
||||
addHostDeviceSupportedProperties(&s.ModuleBase, snapshotModule)
|
||||
for _, memberListProperty := range sdkMemberListProperties {
|
||||
names := memberListProperty.getter(&s.properties)
|
||||
for _, memberListProperty := range s.dynamicSdkMemberTypes.memberListProperties {
|
||||
names := memberListProperty.getter(s.dynamicMemberTypeListProperties)
|
||||
if len(names) > 0 {
|
||||
snapshotModule.AddProperty(memberListProperty.name, builder.versionedSdkMemberNames(names))
|
||||
snapshotModule.AddProperty(memberListProperty.propertyName(), builder.versionedSdkMemberNames(names))
|
||||
}
|
||||
}
|
||||
bpFile.AddModule(snapshotModule)
|
||||
|
Reference in New Issue
Block a user