提交 f5c6dc66 编写于 作者: I Iurii Makhno

Prepare for release 1.4.0.

上级 c975779b
......@@ -26,4 +26,4 @@ https://developer.android.com/studio/command-line/bundletool
## Releases
Latest release: [1.3.0](https://github.com/google/bundletool/releases)
Latest release: [1.4.0](https://github.com/google/bundletool/releases)
release_version = 1.3.0
release_version = 1.4.0
......@@ -64,6 +64,8 @@ import java.util.zip.ZipFile;
@AutoValue
public abstract class BuildBundleCommand {
private static final Logger logger = Logger.getLogger(BuildBundleCommand.class.getName());
public static final String COMMAND_NAME = "build-bundle";
private static final Flag<Path> OUTPUT_FLAG = Flag.path("output");
......@@ -74,8 +76,6 @@ public abstract class BuildBundleCommand {
Flag.mapCollector("metadata-file", ZipPath.class, Path.class);
private static final Flag<Boolean> UNCOMPRESSED_FLAG = Flag.booleanFlag("uncompressed");
private static final Logger logger = Logger.getLogger(BuildBundleCommand.class.getName());
public abstract Path getOutputPath();
public abstract boolean getOverwriteOutput();
......
......@@ -22,13 +22,16 @@ import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static com.google.common.collect.MoreCollectors.toOptional;
import static java.nio.charset.StandardCharsets.UTF_8;
import com.android.bundle.Commands.AssetModuleMetadata;
import com.android.bundle.Commands.AssetSliceSet;
import com.android.bundle.Commands.BuildApksResult;
import com.android.bundle.Commands.DefaultTargetingValue;
import com.android.bundle.Commands.ExtractApksResult;
import com.android.bundle.Commands.ExtractedApk;
import com.android.bundle.Config.SplitDimension.Value;
import com.android.bundle.Devices.DeviceSpec;
import com.android.tools.build.bundletool.commands.CommandHelp.CommandDescription;
import com.android.tools.build.bundletool.commands.CommandHelp.FlagDescription;
......@@ -182,8 +185,14 @@ public abstract class ExtractApksCommand {
BuildApksResult toc = ResultUtils.readTableOfContents(getApksArchivePath());
Optional<ImmutableSet<String>> requestedModuleNames =
getModules().map(modules -> resolveRequestedModules(modules, toc));
ApkMatcher apkMatcher = new ApkMatcher(getDeviceSpec(), requestedModuleNames, getInstant());
DeviceSpec deviceSpec = applyDefaultsToDeviceSpec(getDeviceSpec(), toc);
ApkMatcher apkMatcher =
new ApkMatcher(
deviceSpec,
requestedModuleNames,
getInstant(),
/* ensureDensityAndAbiApksMatched= */ true);
ImmutableList<GeneratedApk> generatedApks = apkMatcher.getMatchingApks(toc);
if (generatedApks.isEmpty()) {
......@@ -308,6 +317,23 @@ public abstract class ExtractApksCommand {
}
}
private static DeviceSpec applyDefaultsToDeviceSpec(DeviceSpec deviceSpec, BuildApksResult toc) {
if (!deviceSpec.getDeviceTier().isEmpty()) {
return deviceSpec;
}
Optional<String> defaultDeviceTier =
toc.getDefaultTargetingValueList().stream()
.filter(
defaultTargetingValue ->
defaultTargetingValue.getDimension().equals(Value.DEVICE_TIER))
.map(DefaultTargetingValue::getDefaultValue)
.collect(toOptional());
if (defaultDeviceTier.isPresent()) {
return deviceSpec.toBuilder().setDeviceTier(defaultDeviceTier.get()).build();
}
return deviceSpec;
}
public static CommandHelp help() {
return CommandHelp.builder()
.setCommandName(COMMAND_NAME)
......
......@@ -26,6 +26,9 @@ import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.common.collect.ImmutableListMultimap.toImmutableListMultimap;
import static com.google.common.collect.ImmutableMap.toImmutableMap;
import static com.google.common.collect.Streams.stream;
import static java.util.Comparator.comparing;
import static java.util.stream.Collectors.groupingBy;
import static java.util.stream.Collectors.maxBy;
import com.android.bundle.Devices.DeviceSpec;
import com.android.tools.build.bundletool.commands.CommandHelp.CommandDescription;
......@@ -53,6 +56,7 @@ import com.google.common.base.Suppliers;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableListMultimap;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Streams;
import com.google.common.io.ByteStreams;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import java.io.IOException;
......@@ -217,23 +221,28 @@ public abstract class InstallMultiApksCommand {
ImmutableMap<String, InstalledPackageInfo> existingPackages =
getPackagesInstalledOnDevice(device);
ImmutableList<InstallableApk> apksToInstall =
ImmutableList<PackagePathVersion> installableApksFilesWithBadgingInfo =
getActualApksPaths(tempDirectory).stream()
.flatMap(
apksArchivePath ->
stream(
apksWithPackageName(apksArchivePath, deviceSpec, aapt2CommandSupplier)))
.filter(apk -> shouldInstall(apk, existingPackages))
.filter(apks -> shouldInstall(apks, existingPackages))
.collect(toImmutableList());
ImmutableList<PackagePathVersion> apkFilesToInstall =
uniqueApksByPackageName(installableApksFilesWithBadgingInfo).stream()
.flatMap(apks -> extractApkListFromApks(deviceSpec, apks, tempDirectory).stream())
.collect(toImmutableList());
ImmutableListMultimap<String, String> apkToInstallByPackage =
apksToInstall.stream()
apkFilesToInstall.stream()
.collect(
toImmutableListMultimap(
InstallableApk::getPackageName,
installableApk -> installableApk.getPath().toAbsolutePath().toString()));
PackagePathVersion::getPackageName,
packagePathVersion ->
packagePathVersion.getPath().toAbsolutePath().toString()));
if (apksToInstall.isEmpty()) {
if (apkFilesToInstall.isEmpty()) {
logger.warning("No packages found to install! Exiting...");
return;
}
......@@ -257,7 +266,7 @@ public abstract class InstallMultiApksCommand {
* </ul>
*/
private boolean shouldInstall(
InstallableApk apk, ImmutableMap<String, InstalledPackageInfo> existingPackages) {
PackagePathVersion apk, ImmutableMap<String, InstalledPackageInfo> existingPackages) {
if (getUpdateOnly() && !existingPackages.containsKey(apk.getPackageName())) {
logger.info(
String.format(
......@@ -310,7 +319,7 @@ public abstract class InstallMultiApksCommand {
installedPackageInfo -> installedPackageInfo));
}
private static Optional<InstallableApk> apksWithPackageName(
private static Optional<PackagePathVersion> apksWithPackageName(
Path apksArchivePath, DeviceSpec deviceSpec, Supplier<Aapt2Command> aapt2CommandSupplier) {
try (TempDirectory tempDirectory = new TempDirectory()) {
// Any of the extracted .apk/.apex files will work.
......@@ -326,7 +335,7 @@ public abstract class InstallMultiApksCommand {
BadgingInfo badgingInfo =
BadgingInfoParser.parse(aapt2CommandSupplier.get().dumpBadging(extractedFile));
return Optional.of(
InstallableApk.create(
PackagePathVersion.create(
apksArchivePath, badgingInfo.getPackageName(), badgingInfo.getVersionCode()));
} catch (IncompatibleDeviceException e) {
logger.warning(
......@@ -355,8 +364,8 @@ public abstract class InstallMultiApksCommand {
}
/** Extracts the apk/apex files that will be installed from a given .apks. */
private static ImmutableList<InstallableApk> extractApkListFromApks(
DeviceSpec deviceSpec, InstallableApk apksArchive, TempDirectory tempDirectory) {
private static ImmutableList<PackagePathVersion> extractApkListFromApks(
DeviceSpec deviceSpec, PackagePathVersion apksArchive, TempDirectory tempDirectory) {
logger.info(String.format("Extracting package '%s'", apksArchive.getPackageName()));
try {
Path output = tempDirectory.getPath().resolve(apksArchive.getPackageName());
......@@ -371,7 +380,7 @@ public abstract class InstallMultiApksCommand {
return extractApksCommand.build().execute().stream()
.map(
path ->
InstallableApk.create(
PackagePathVersion.create(
path, apksArchive.getPackageName(), apksArchive.getVersionCode()))
.collect(toImmutableList());
} catch (IncompatibleDeviceException e) {
......@@ -411,8 +420,8 @@ public abstract class InstallMultiApksCommand {
.collect(toImmutableList());
for (ZipEntry apksToExtract : apksToExtractList) {
Path extractedApksPath =
zipExtractedSubDirectory.resolve(
ZipPath.create(apksToExtract.getName()).getFileName().toString());
zipExtractedSubDirectory.resolve(ZipPath.create(apksToExtract.getName()).toString());
Files.createDirectories(extractedApksPath.getParent());
try (InputStream inputStream = apksArchiveContainer.getInputStream(apksToExtract);
OutputStream outputApks = Files.newOutputStream(extractedApksPath)) {
ByteStreams.copy(inputStream, outputApks);
......@@ -423,6 +432,23 @@ public abstract class InstallMultiApksCommand {
return extractedApks.build();
}
/**
* If multiple APKS files for the same package are present and installable, only the APKS with
* higher version code should be installed.
*/
private static ImmutableList<PackagePathVersion> uniqueApksByPackageName(
ImmutableList<PackagePathVersion> installableApksFiles) {
return installableApksFiles.stream()
.collect(
groupingBy(
PackagePathVersion::getPackageName,
maxBy(comparing(PackagePathVersion::getVersionCode))))
.values()
.stream()
.flatMap(Streams::stream)
.collect(toImmutableList());
}
public static CommandHelp help() {
return CommandHelp.builder()
.setCommandName(COMMAND_NAME)
......@@ -519,12 +545,13 @@ public abstract class InstallMultiApksCommand {
return CommandUtils.extractAapt2FromJar(tempDirectoryForJarCommand);
}
/** Represents pair of APK path and package name of this APK. */
/** Represents a Package with a path to a relevant file and a version code. */
@AutoValue
public abstract static class InstallableApk {
abstract static class PackagePathVersion {
public static InstallableApk create(Path path, String packageName, long versionCode) {
return new AutoValue_InstallMultiApksCommand_InstallableApk(path, packageName, versionCode);
public static PackagePathVersion create(Path path, String packageName, long versionCode) {
return new AutoValue_InstallMultiApksCommand_PackagePathVersion(
path, packageName, versionCode);
}
public abstract Path getPath();
......@@ -533,6 +560,6 @@ public abstract class InstallMultiApksCommand {
public abstract long getVersionCode();
InstallableApk() {}
PackagePathVersion() {}
}
}
......@@ -16,10 +16,10 @@
package com.android.tools.build.bundletool.device;
import static com.android.tools.build.bundletool.model.utils.ModuleDependenciesUtils.addModuleDependencies;
import static com.android.tools.build.bundletool.model.utils.ModuleDependenciesUtils.buildAdjacencyMap;
import static com.android.tools.build.bundletool.model.utils.ModuleDependenciesUtils.getModulesIncludingDependencies;
import static com.android.tools.build.bundletool.model.version.VersionGuardedFeature.NEW_DELIVERY_TYPE_MANIFEST_TAG;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import com.android.bundle.Commands.ApkDescription;
......@@ -33,21 +33,19 @@ import com.android.bundle.Commands.Variant;
import com.android.bundle.Devices.DeviceSpec;
import com.android.bundle.Targeting.ApkTargeting;
import com.android.tools.build.bundletool.model.ModuleSplit;
import com.android.tools.build.bundletool.model.OptimizationDimension;
import com.android.tools.build.bundletool.model.ZipPath;
import com.android.tools.build.bundletool.model.exceptions.IncompatibleDeviceException;
import com.android.tools.build.bundletool.model.exceptions.InvalidCommandException;
import com.android.tools.build.bundletool.model.version.Version;
import com.google.auto.value.AutoValue;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMultimap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
import java.util.Collection;
import java.util.HashSet;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Stream;
/** Calculates whether a given device can be served an APK generated by the bundle tool. */
public class ApkMatcher {
......@@ -58,19 +56,31 @@ public class ApkMatcher {
private final boolean matchInstant;
private final ModuleMatcher moduleMatcher;
private final VariantMatcher variantMatcher;
private final boolean ensureDensityAndAbiApksMatched;
public ApkMatcher(DeviceSpec deviceSpec) {
this(deviceSpec, Optional.empty(), /* matchInstant= */ false);
this(
deviceSpec,
Optional.empty(),
/* matchInstant= */ false,
/* ensureDensityAndAbiApksMatched= */ false);
}
/**
* Constructs an ApkMatcher, with a given device configuration, set of splits to match, and an
* option to match instant splits.
* Constructs an ApkMatcher.
*
* @param deviceSpec given device configuration
* @param requestedModuleNames sets of modules to match, all modules if empty
* @param matchInstant when set, matches APKs for instant modules only
* @param ensureDensityAndAbiApksMatched when set, ensures one density split and/or one ABI split
* are matched per each module (if module has such splits) and throws
* IncompatibleDeviceException if not
*/
public ApkMatcher(
DeviceSpec deviceSpec,
Optional<ImmutableSet<String>> requestedModuleNames,
boolean matchInstant) {
boolean matchInstant,
boolean ensureDensityAndAbiApksMatched) {
checkArgument(
!requestedModuleNames.isPresent() || !requestedModuleNames.get().isEmpty(),
"Set of requested split modules cannot be empty.");
......@@ -95,6 +105,7 @@ public class ApkMatcher {
deviceTierMatcher);
this.requestedModuleNames = requestedModuleNames;
this.matchInstant = matchInstant;
this.ensureDensityAndAbiApksMatched = ensureDensityAndAbiApksMatched;
this.moduleMatcher = new ModuleMatcher(sdkVersionMatcher, deviceFeatureMatcher);
this.variantMatcher =
new VariantMatcher(
......@@ -115,9 +126,7 @@ public class ApkMatcher {
public ImmutableList<GeneratedApk> getMatchingApks(BuildApksResult buildApksResult) {
Optional<Variant> matchingVariant = variantMatcher.getMatchingVariant(buildApksResult);
if (matchingVariant.isPresent()) {
validateVariant(matchingVariant.get(), buildApksResult);
}
matchingVariant.ifPresent(variant -> validateVariant(variant, buildApksResult));
ImmutableList<GeneratedApk> variantApks =
matchingVariant.isPresent()
......@@ -136,62 +145,104 @@ public class ApkMatcher {
public ImmutableList<GeneratedApk> getMatchingApksFromVariant(
Variant variant, Version bundleVersion) {
ImmutableList.Builder<GeneratedApk> matchedApksBuilder = ImmutableList.builder();
Predicate<String> moduleNameMatcher = getModuleNameMatcher(variant, bundleVersion);
for (ApkSet apkSet : variant.getApkSetList()) {
String moduleName = apkSet.getModuleMetadata().getName();
for (ApkDescription apkDescription : apkSet.getApkDescriptionList()) {
ApkTargeting apkTargeting = apkDescription.getTargeting();
boolean isSplit =
!apkDescription.hasStandaloneApkMetadata() && !apkDescription.hasApexApkMetadata();
ImmutableSet<String> modulesToMatch =
matchInstant
? getRequestedInstantModulesWithDependencies(variant)
: getInstallTimeAndRequestedModulesWithDependencies(variant, bundleVersion);
checkCompatibleWithApkTargeting(apkTargeting);
return variant.getApkSetList().stream()
.filter(apkSet -> modulesToMatch.contains(apkSet.getModuleMetadata().getName()))
.flatMap(apkSet -> getMatchingApksFromModule(apkSet).stream())
.collect(toImmutableList());
}
if (matchesApk(apkTargeting, isSplit, moduleName, moduleNameMatcher)) {
matchedApksBuilder.add(
GeneratedApk.create(
ZipPath.create(apkDescription.getPath()),
moduleName,
apkSet.getModuleMetadata().getDeliveryType()));
}
private ImmutableList<GeneratedApk> getMatchingApksFromModule(ApkSet moduleApks) {
String moduleName = moduleApks.getModuleMetadata().getName();
ImmutableList<ApkDescription> matchedApks =
moduleApks.getApkDescriptionList().stream()
.peek(apkDescription -> checkCompatibleWithApkTargeting(apkDescription.getTargeting()))
.filter(apkDescription -> matchesApkTargeting(apkDescription.getTargeting()))
.collect(toImmutableList());
if (ensureDensityAndAbiApksMatched) {
ImmutableSet<OptimizationDimension> availableDimensions =
getApkTargetingOnlyAbiAndDensity(moduleApks.getApkDescriptionList());
ImmutableSet<OptimizationDimension> matchedDimensions =
getApkTargetingOnlyAbiAndDensity(matchedApks);
if (!availableDimensions.equals(matchedDimensions)) {
throw IncompatibleDeviceException.builder()
.withUserMessage(
"Missing APKs for %s dimensions in the module '%s' for the provided device.",
Sets.difference(availableDimensions, matchedDimensions), moduleName)
.build();
}
}
return matchedApksBuilder.build();
return matchedApks.stream()
.map(
apkDescription ->
GeneratedApk.create(
ZipPath.create(apkDescription.getPath()),
moduleName,
moduleApks.getModuleMetadata().getDeliveryType()))
.collect(toImmutableList());
}
private Predicate<String> getModuleNameMatcher(Variant variant, Version bundleVersion) {
if (requestedModuleNames.isPresent()) {
ImmutableMultimap<String, String> moduleDependenciesMap = buildAdjacencyMap(variant);
HashSet<String> dependencyModules = new HashSet<>(requestedModuleNames.get());
for (String requestedModuleName : requestedModuleNames.get()) {
addModuleDependencies(requestedModuleName, moduleDependenciesMap, dependencyModules);
}
private static ImmutableSet<OptimizationDimension> getApkTargetingOnlyAbiAndDensity(
Collection<ApkDescription> apks) {
return apks.stream()
.map(ApkDescription::getTargeting)
.flatMap(
targeting -> {
Stream.Builder<OptimizationDimension> dimensions = Stream.builder();
if (targeting.hasAbiTargeting()) {
dimensions.add(OptimizationDimension.ABI);
}
if (targeting.hasScreenDensityTargeting()) {
dimensions.add(OptimizationDimension.SCREEN_DENSITY);
}
return dimensions.build();
})
.collect(toImmutableSet());
}
if (matchInstant) {
return dependencyModules::contains;
} else {
return Predicates.or(
buildModulesDeliveredInstallTime(variant, bundleVersion)::contains,
dependencyModules::contains);
}
} else {
if (matchInstant) {
// For instant matching, by default all instant modules are matched.
return Predicates.alwaysTrue();
} else {
// For conventional matching, only install-time modules are matched.
return buildModulesDeliveredInstallTime(variant, bundleVersion)::contains;
}
private ImmutableSet<String> getRequestedInstantModulesWithDependencies(Variant variant) {
if (!requestedModuleNames.isPresent()) {
// For instant matching, by default all modules from instant variant are matched.
return variant.getApkSetList().stream()
.map(apkSet -> apkSet.getModuleMetadata().getName())
.collect(toImmutableSet());
}
return getModulesIncludingDependencies(variant, requestedModuleNames.get());
}
private ImmutableSet<String> getInstallTimeAndRequestedModulesWithDependencies(
Variant variant, Version bundleVersion) {
ImmutableSet<String> installTimeModules =
buildModulesDeliveredInstallTime(variant, bundleVersion);
ImmutableSet<String> explicitlyRequested = requestedModuleNames.orElse(ImmutableSet.of());
// Always return all install time modules plus requested ones.
return getModulesIncludingDependencies(
variant, Sets.union(installTimeModules, explicitlyRequested));
}
private void validateVariant(Variant variant, BuildApksResult buildApksResult) {
if (requestedModuleNames.isPresent()) {
boolean isStandaloneVariant =
variant.getApkSetList().stream()
.flatMap(apkSet -> apkSet.getApkDescriptionList().stream())
.anyMatch(
apkDescription ->
apkDescription.hasStandaloneApkMetadata()
|| apkDescription.hasApexApkMetadata());
if (isStandaloneVariant) {
throw InvalidCommandException.builder()
.withInternalMessage("Cannot restrict modules when the device matches a non-split APK.")
.build();
}
Set<String> availableModules =
Sets.union(
variant.getApkSetList().stream()
......@@ -233,26 +284,6 @@ public class ApkMatcher {
return installTime && moduleMatcher.matchesModuleTargeting(moduleMetadata.getTargeting());
}
/** Returns whether a given APK generated by the Bundle Tool should be installed on a device. */
private boolean matchesApk(
ApkTargeting apkTargeting,
boolean isSplit,
String moduleName,
Predicate<String> moduleNameMatcher) {
boolean matchesTargeting = matchesApkTargeting(apkTargeting);
if (isSplit) {
return matchesTargeting && moduleNameMatcher.test(moduleName);
} else {
if (matchesTargeting && requestedModuleNames.isPresent()) {
throw InvalidCommandException.builder()
.withInternalMessage("Cannot restrict modules when the device matches a non-split APK.")
.build();
}
return matchesTargeting;
}
}
private boolean matchesApkTargeting(ApkTargeting apkTargeting) {
return apkMatchers.stream()
.allMatch(matcher -> matcher.getApkTargetingPredicate().test(apkTargeting));
......@@ -286,51 +317,44 @@ public class ApkMatcher {
apkMatchers.forEach(matcher -> checkCompatibleWithApkTargetingHelper(matcher, apkTargeting));
}
private <T> void checkCompatibleWithApkTargetingHelper(
private static <T> void checkCompatibleWithApkTargetingHelper(
TargetingDimensionMatcher<T> matcher, ApkTargeting apkTargeting) {
matcher.checkDeviceCompatible(matcher.getTargetingValue(apkTargeting));
}
public ImmutableList<GeneratedApk> getMatchingApksFromAssetModules(
Collection<AssetSliceSet> assetModules) {
ImmutableList.Builder<GeneratedApk> matchedApksBuilder = ImmutableList.builder();
Predicate<String> assetModuleNameMatcher = getAssetModuleNameMatcher(assetModules);
for (AssetSliceSet sliceSet : assetModules) {
String moduleName = sliceSet.getAssetModuleMetadata().getName();
for (ApkDescription apkDescription : sliceSet.getApkDescriptionList()) {
ApkTargeting apkTargeting = apkDescription.getTargeting();
if (matchesApk(apkTargeting, /*isSplit=*/ true, moduleName, assetModuleNameMatcher)) {
matchedApksBuilder.add(
GeneratedApk.create(
ZipPath.create(apkDescription.getPath()),
moduleName,
sliceSet.getAssetModuleMetadata().getDeliveryType()));
}
}
}
return matchedApksBuilder.build();
ImmutableSet<String> assetModulesToMatch =
requestedModuleNames.orElseGet(() -> getUpfrontAssetModules(assetModules));
return assetModules.stream()
.filter(
assetModule ->
assetModulesToMatch.contains(assetModule.getAssetModuleMetadata().getName()))
.flatMap(
assetModule ->
assetModule.getApkDescriptionList().stream()
.filter(apkDescription -> matchesApkTargeting(apkDescription.getTargeting()))
.map(
apkDescription ->
GeneratedApk.create(
ZipPath.create(apkDescription.getPath()),
assetModule.getAssetModuleMetadata().getName(),
assetModule.getAssetModuleMetadata().getDeliveryType())))
.collect(toImmutableList());
}
private Predicate<String> getAssetModuleNameMatcher(Collection<AssetSliceSet> assetModules) {
if (requestedModuleNames.isPresent()) {
return requestedModuleNames.get()::contains;
}
ImmutableSet<String> upfrontAssetModuleNames =
assetModules.stream()
.filter(
sliceSet ->
sliceSet
.getAssetModuleMetadata()
.getDeliveryType()
.equals(DeliveryType.INSTALL_TIME))
.map(sliceSet -> sliceSet.getAssetModuleMetadata().getName())
.collect(toImmutableSet());