| // Copyright 2021 The Bazel Authors. 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 com.google.devtools.build.lib.cmdline; |
| |
| import com.google.common.base.Objects; |
| import com.google.common.base.Preconditions; |
| import com.google.common.collect.ImmutableMap; |
| import com.google.common.collect.Maps; |
| import java.util.HashMap; |
| import java.util.Map; |
| import java.util.Map.Entry; |
| import java.util.Optional; |
| import javax.annotation.Nullable; |
| |
| /** |
| * Stores the mapping from apparent repo name to canonical repo name, from the viewpoint of an |
| * "owner repo". |
| * |
| * <p>For repositories from the WORKSPACE file, if the requested repo doesn't exist in the mapping, |
| * we fallback to the requested name. For repositories from Bzlmod, we return null to let the caller |
| * decide what to do. |
| */ |
| public class RepositoryMapping { |
| /* A repo mapping that always falls back to using the apparent name as the canonical name. */ |
| public static final RepositoryMapping ALWAYS_FALLBACK = createAllowingFallback(ImmutableMap.of()); |
| |
| private final ImmutableMap<String, RepositoryName> entries; |
| @Nullable private final RepositoryName ownerRepo; |
| |
| private RepositoryMapping( |
| Map<String, RepositoryName> entries, @Nullable RepositoryName ownerRepo) { |
| this.entries = ImmutableMap.copyOf(entries); |
| this.ownerRepo = ownerRepo; |
| } |
| |
| /** Returns all the entries in this repo mapping. */ |
| public final ImmutableMap<String, RepositoryName> entries() { |
| return entries; |
| } |
| |
| /** |
| * The owner repo of this repository mapping. It is for providing useful debug information when |
| * repository mapping fails due to enforcing strict dependency, therefore it's only recorded when |
| * we don't fall back to the requested repo name. |
| */ |
| @Nullable |
| public final RepositoryName ownerRepo() { |
| return ownerRepo; |
| } |
| |
| @Override |
| public boolean equals(Object o) { |
| if (this == o) { |
| return true; |
| } |
| if (!(o instanceof RepositoryMapping)) { |
| return false; |
| } |
| RepositoryMapping that = (RepositoryMapping) o; |
| return Objects.equal(entries, that.entries) && Objects.equal(ownerRepo, that.ownerRepo); |
| } |
| |
| @Override |
| public int hashCode() { |
| return Objects.hashCode(entries, ownerRepo); |
| } |
| |
| public static RepositoryMapping create( |
| Map<String, RepositoryName> entries, RepositoryName ownerRepo) { |
| return new RepositoryMapping( |
| Preconditions.checkNotNull(entries), Preconditions.checkNotNull(ownerRepo)); |
| } |
| |
| public static RepositoryMapping createAllowingFallback(Map<String, RepositoryName> entries) { |
| return new RepositoryMapping(Preconditions.checkNotNull(entries), null); |
| } |
| |
| /** |
| * Create a new {@link RepositoryMapping} instance based on existing repo mappings and given |
| * additional mappings. If there are conflicts, existing mappings will take precedence. |
| */ |
| public RepositoryMapping withAdditionalMappings(Map<String, RepositoryName> additionalMappings) { |
| HashMap<String, RepositoryName> allMappings = new HashMap<>(additionalMappings); |
| allMappings.putAll(entries()); |
| return new RepositoryMapping(allMappings, ownerRepo()); |
| } |
| |
| /** |
| * Create a new {@link RepositoryMapping} instance based on existing repo mappings and given |
| * additional mappings. If there are conflicts, existing mappings will take precedence. The owner |
| * repo of the given additional mappings is ignored. |
| */ |
| public RepositoryMapping withAdditionalMappings(RepositoryMapping additionalMappings) { |
| return withAdditionalMappings(additionalMappings.entries()); |
| } |
| |
| /** |
| * Returns the canonical repository name associated with the given apparent repo name. The |
| * provided apparent repo name is assumed to be valid. |
| */ |
| public RepositoryName get(String preMappingName) { |
| RepositoryName canonicalRepoName = entries().get(preMappingName); |
| if (canonicalRepoName != null) { |
| return canonicalRepoName; |
| } |
| // If the owner repo is not present, that means we should fall back to the requested repo name. |
| if (ownerRepo() == null) { |
| return RepositoryName.createUnvalidated(preMappingName); |
| } else { |
| return RepositoryName.createUnvalidated(preMappingName).toNonVisible(ownerRepo()); |
| } |
| } |
| |
| /** |
| * Whether the repo with this mapping is subject to strict deps; when strict deps is off, unknown |
| * apparent names are silently treated as canonical names. |
| */ |
| public boolean usesStrictDeps() { |
| return ownerRepo() != null; |
| } |
| |
| /** |
| * Returns the first apparent name in this mapping that maps to the given canonical name, if any. |
| */ |
| public Optional<String> getInverse(RepositoryName postMappingName) { |
| return entries().entrySet().stream() |
| .filter(e -> e.getValue().equals(postMappingName)) |
| .map(Entry::getKey) |
| .findFirst(); |
| } |
| |
| /** |
| * Creates a new {@link RepositoryMapping} instance that is the equivalent of composing this |
| * {@link RepositoryMapping} with another one. That is, {@code a.composeWith(b).get(name) === |
| * b.get(a.get(name))} (treating {@code b} as allowing fallback). |
| * |
| * <p>Since we're treating the result of {@code a.get(name)} as an apparent repo name instead of a |
| * canonical repo name, this only really makes sense when {@code a} does not use strict deps (i.e. |
| * allows fallback). |
| */ |
| public RepositoryMapping composeWith(RepositoryMapping other) { |
| Preconditions.checkArgument( |
| !usesStrictDeps(), "only an allow-fallback mapping can be composed with other mappings"); |
| HashMap<String, RepositoryName> entries = new HashMap<>(other.entries()); |
| for (Map.Entry<String, RepositoryName> entry : entries().entrySet()) { |
| RepositoryName mappedName = other.get(entry.getValue().getName()); |
| entries.put(entry.getKey(), mappedName.isVisible() ? mappedName : entry.getValue()); |
| } |
| return new RepositoryMapping(entries, null); |
| } |
| |
| /** |
| * Returns a new {@link RepositoryMapping} instance with identical contents, except that the |
| * inverse mapping is cached, causing {@link #getInverse} to be much more efficient. This is |
| * particularly important for the main repo mapping, as it's often used to generate display-form |
| * labels ({@link Label#getDisplayForm}). |
| */ |
| public RepositoryMapping withCachedInverseMap() { |
| var inverse = Maps.<RepositoryName, String>newHashMapWithExpectedSize(entries.size()); |
| for (Map.Entry<String, RepositoryName> entry : entries.entrySet()) { |
| inverse.putIfAbsent(entry.getValue(), entry.getKey()); |
| } |
| var inverseCopy = ImmutableMap.copyOf(inverse); |
| return new RepositoryMapping(entries, ownerRepo) { |
| @Override |
| public Optional<String> getInverse(RepositoryName postMappingName) { |
| return Optional.ofNullable(inverseCopy.get(postMappingName)); |
| } |
| }; |
| } |
| } |