PythonUtils.GetInitPyFiles gains a Predicate<PathFragment> supplied at construction time so that other implementations can supply their own logic for determining when a file is a Python package defining __init__.py equivalent, meaning none need be created.

Adds PythonSemantics.getEmptyRunFilesSupplier() and moves the static PythonUtils.GET_INIT_PY_FILES into the BazelPythonSemantics implementation in favor of PyExecutable calling the abstract getEmptyRunfilesSupplier() so that alternate PythonSemantics implementations can supply their own behavior.

PiperOrigin-RevId: 317142545
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/rules/python/BazelPythonSemantics.java b/src/main/java/com/google/devtools/build/lib/bazel/rules/python/BazelPythonSemantics.java
index 14f1dce..70db056 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/rules/python/BazelPythonSemantics.java
+++ b/src/main/java/com/google/devtools/build/lib/bazel/rules/python/BazelPythonSemantics.java
@@ -49,18 +49,23 @@
 import com.google.devtools.build.lib.rules.python.PyRuntimeInfo;
 import com.google.devtools.build.lib.rules.python.PythonConfiguration;
 import com.google.devtools.build.lib.rules.python.PythonSemantics;
+import com.google.devtools.build.lib.rules.python.PythonUtils;
 import com.google.devtools.build.lib.rules.python.PythonVersion;
 import com.google.devtools.build.lib.util.FileTypeSet;
 import com.google.devtools.build.lib.util.OS;
 import com.google.devtools.build.lib.vfs.PathFragment;
+import java.io.Serializable;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.List;
+import java.util.function.Predicate;
 import javax.annotation.Nullable;
 
 /** Functionality specific to the Python rules in Bazel. */
 public class BazelPythonSemantics implements PythonSemantics {
 
+  public static final Runfiles.EmptyFilesSupplier GET_INIT_PY_FILES =
+      new PythonUtils.GetInitPyFiles((Predicate<PathFragment> & Serializable) source -> false);
   private static final Template STUB_TEMPLATE =
       Template.forResource(BazelPythonSemantics.class, "python_stub_template.txt");
   public static final InstrumentationSpec PYTHON_COLLECTION_SPEC = new InstrumentationSpec(
@@ -70,6 +75,11 @@
   public static final PathFragment ZIP_RUNFILES_DIRECTORY_NAME = PathFragment.create("runfiles");
 
   @Override
+  public Runfiles.EmptyFilesSupplier getEmptyRunfilesSupplier() {
+    return GET_INIT_PY_FILES;
+  }
+
+  @Override
   public String getSrcsVersionDocURL() {
     // TODO(#8996): Update URL to point to rules_python's docs instead of the Bazel site.
     return "https://docs.bazel.build/versions/master/be/python.html#py_binary.srcs_version";
diff --git a/src/main/java/com/google/devtools/build/lib/rules/python/PyExecutable.java b/src/main/java/com/google/devtools/build/lib/rules/python/PyExecutable.java
index f0f6ad2..bb47f6b 100644
--- a/src/main/java/com/google/devtools/build/lib/rules/python/PyExecutable.java
+++ b/src/main/java/com/google/devtools/build/lib/rules/python/PyExecutable.java
@@ -126,7 +126,8 @@
    *
    * <p>See {@link PythonUtils#getInitPyFiles} for details about how the files are created.
    */
-  private static void maybeCreateInitFiles(RuleContext ruleContext, Runfiles.Builder builder) {
+  private static void maybeCreateInitFiles(
+      RuleContext ruleContext, Runfiles.Builder builder, PythonSemantics semantics) {
     boolean createFiles;
     if (!ruleContext.attributes().has("legacy_create_init", BuildType.TRISTATE)) {
       createFiles = true;
@@ -139,7 +140,7 @@
       }
     }
     if (createFiles) {
-      builder.setEmptyFilesSupplier(PythonUtils.GET_INIT_PY_FILES);
+      builder.setEmptyFilesSupplier(semantics.getEmptyRunfilesSupplier());
     }
   }
 
@@ -157,7 +158,7 @@
     semantics.collectDefaultRunfiles(ruleContext, builder);
     builder.add(ruleContext, PythonRunfilesProvider.TO_RUNFILES);
 
-    maybeCreateInitFiles(ruleContext, builder);
+    maybeCreateInitFiles(ruleContext, builder, semantics);
 
     semantics.collectRunfilesForBinary(ruleContext, builder, common, ccInfo);
     return builder.build();
diff --git a/src/main/java/com/google/devtools/build/lib/rules/python/PythonSemantics.java b/src/main/java/com/google/devtools/build/lib/rules/python/PythonSemantics.java
index 4225a4c..5a055ce 100644
--- a/src/main/java/com/google/devtools/build/lib/rules/python/PythonSemantics.java
+++ b/src/main/java/com/google/devtools/build/lib/rules/python/PythonSemantics.java
@@ -93,4 +93,12 @@
       throws InterruptedException, RuleErrorException;
 
   CcInfo buildCcInfoProvider(Iterable<? extends TransitiveInfoCollection> deps);
+
+  /**
+   * Called when building executables or packages to fill in missing empty __init__.py files if the
+   * --incompatible_default_to_explicit_init_py has not yet been enabled. This usually returns a
+   * public static final reference, code is free to use that directly on specific implementations
+   * instead of making this call.
+   */
+  Runfiles.EmptyFilesSupplier getEmptyRunfilesSupplier();
 }
diff --git a/src/main/java/com/google/devtools/build/lib/rules/python/PythonUtils.java b/src/main/java/com/google/devtools/build/lib/rules/python/PythonUtils.java
index 4a80c32..49a9415 100644
--- a/src/main/java/com/google/devtools/build/lib/rules/python/PythonUtils.java
+++ b/src/main/java/com/google/devtools/build/lib/rules/python/PythonUtils.java
@@ -22,18 +22,16 @@
 import com.google.devtools.build.lib.analysis.TransitionMode;
 import com.google.devtools.build.lib.analysis.actions.CustomCommandLine;
 import com.google.devtools.build.lib.analysis.actions.SpawnAction;
-import com.google.devtools.build.lib.skyframe.serialization.autocodec.AutoCodec;
 import com.google.devtools.build.lib.util.FileType;
 import com.google.devtools.build.lib.vfs.PathFragment;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Map;
 import java.util.Set;
+import java.util.function.Predicate;
 import javax.annotation.Nullable;
 
-/**
- * Various utility methods for Python support.
- */
+/** Various utility methods for Python support. */
 public final class PythonUtils {
   public static final PathFragment INIT_PY = PathFragment.create("__init__.py");
   public static final PathFragment INIT_PYC = PathFragment.create("__init__.pyc");
@@ -41,48 +39,75 @@
 
   private static final FileType REQUIRES_INIT_PY = FileType.of(".py", ".so", ".pyc");
 
-  static class GetInitPyFiles implements Runfiles.EmptyFilesSupplier {
+  /**
+   * Used to get the set of empty __init__.py files to be added to a given set of files to allow the
+   * Python runtime to import subdirectories potentially containing Python code to be imported as
+   * packages. Ideally this feature goes away with --incompatible_default_to_explicit_init_py as the
+   * long term default behavior.
+   */
+  public static class GetInitPyFiles implements Runfiles.EmptyFilesSupplier {
+    private final Predicate<PathFragment> isPackageInit;
+
+    /**
+     * The Predicate isPackageInit's .test(source) should be true when a given source is known to be
+     * a valid __init__.py file equivalent, meaning no empty __init__.py file need be created.
+     * Useful for custom Python runtimes that may have non-standard Python package import logic.
+     */
+    public GetInitPyFiles(Predicate<PathFragment> isPackageInit) {
+      this.isPackageInit = isPackageInit;
+    }
+
     @Override
-    public Iterable<PathFragment> getExtraPaths(Set<PathFragment> manifestPaths) {
+    public Set<PathFragment> getExtraPaths(Set<PathFragment> manifestPaths) {
       return getInitPyFiles(manifestPaths);
     }
-  }
 
-  @AutoCodec
-  public static final Runfiles.EmptyFilesSupplier GET_INIT_PY_FILES = new GetInitPyFiles();
+    /**
+     * Returns the set of empty __init__.py(c) files to be added to a given set of files to allow
+     * the Python runtime to find the <code>.py</code> and <code>.so</code> files present in the
+     * tree.
+     */
+    private ImmutableSet<PathFragment> getInitPyFiles(Set<PathFragment> manifestFiles) {
+      Set<PathFragment> result = new HashSet<>();
+      // A set of directories that already have package init files.
+      Set<PathFragment> hasPackageInitDirs = new HashSet<>(); // For b/142135992.
 
-  private PythonUtils() {
-    // This is a utility class, not to be instantiated
-  }
+      // Find directories containing Python package init files based on a caller supplied test in
+      // order to support non-standard Python package init naming schemes.
+      // This loop is done prior to the one below as we assume no order in the set and that we may
+      // find inits in parent directories listed after subdirectories which the nested loop below
+      // would need to know of.
+      for (PathFragment source : manifestFiles) {
+        if (isPackageInit.test(source)) {
+          hasPackageInitDirs.add(source.getParentDirectory());
+        }
+      }
 
-  /**
-   * Returns the set of empty __init__.py(c) files to be added to a given set of files to allow
-   * the Python runtime to find the <code>.py</code> and <code>.so</code> files present in the
-   * tree.
-   */
-  public static Set<PathFragment> getInitPyFiles(Set<PathFragment> manifestFiles) {
-    Set<PathFragment> result = new HashSet<>();
+      for (PathFragment source : manifestFiles) {
+        // If we have a python or .so file at this level...
+        if (REQUIRES_INIT_PY.matches(source)) {
+          // ...then record that we need an __init__.py in this and all parents directories...
+          while (source.segmentCount() > 1) {
+            source = source.getParentDirectory();
+            // ...unless it's a Python .pyc cache or we already have __init__ there.
+            if (!source.endsWith(PYCACHE) && !hasPackageInitDirs.contains(source)) {
+              PathFragment initpy = source.getRelative(INIT_PY);
+              PathFragment initpyc = source.getRelative(INIT_PYC);
 
-    for (PathFragment source : manifestFiles) {
-      // If we have a python or .so file at this level...
-      if (REQUIRES_INIT_PY.matches(source)) {
-        // ...then record that we need an __init__.py in this and all parents directories...
-        while (source.segmentCount() > 1) {
-          source = source.getParentDirectory();
-          // ...unless it's a Python .pyc cache or we already have __init__ there.
-          if (!source.endsWith(PYCACHE)) {
-            PathFragment initpy = source.getRelative(INIT_PY);
-            PathFragment initpyc = source.getRelative(INIT_PYC);
-
-            if (!manifestFiles.contains(initpy) && !manifestFiles.contains(initpyc)) {
-              result.add(initpy);
+              if (!manifestFiles.contains(initpy) && !manifestFiles.contains(initpyc)) {
+                result.add(initpy);
+              }
             }
           }
         }
       }
-    }
 
-    return ImmutableSet.copyOf(result);
+      return ImmutableSet.copyOf(result);
+    }
+  } // class GetInitPyFiles
+
+  private PythonUtils() {
+    // This is a utility class, not to be instantiated.
   }
 
   /**
diff --git a/src/test/java/com/google/devtools/build/lib/analysis/SourceManifestActionTest.java b/src/test/java/com/google/devtools/build/lib/analysis/SourceManifestActionTest.java
index d90ef90..96fa7dc 100644
--- a/src/test/java/com/google/devtools/build/lib/analysis/SourceManifestActionTest.java
+++ b/src/test/java/com/google/devtools/build/lib/analysis/SourceManifestActionTest.java
@@ -24,7 +24,6 @@
 import com.google.devtools.build.lib.actions.util.ActionsTestUtil;
 import com.google.devtools.build.lib.analysis.SourceManifestAction.ManifestType;
 import com.google.devtools.build.lib.analysis.util.BuildViewTestCase;
-import com.google.devtools.build.lib.rules.python.PythonUtils;
 import com.google.devtools.build.lib.util.Fingerprint;
 import com.google.devtools.build.lib.vfs.Path;
 import com.google.devtools.build.lib.vfs.PathFragment;
@@ -106,7 +105,8 @@
     Runfiles.Builder builder = new Runfiles.Builder("TESTING", false);
     builder.addSymlinks(fakeManifest);
     if (addInitPy) {
-      builder.setEmptyFilesSupplier(PythonUtils.GET_INIT_PY_FILES);
+      builder.setEmptyFilesSupplier(
+          analysisMock.pySupport().getPythonSemantics().getEmptyRunfilesSupplier());
     }
     return new SourceManifestAction(type, NULL_ACTION_OWNER, manifestOutputFile, builder.build());
   }
@@ -313,16 +313,6 @@
     assertThat(computeKey(action2)).isNotEqualTo(computeKey(action1));
   }
 
-  /**
-   * Constructs a new manifest file artifact with the given name, writes the given contents
-   * to that file, and returns the artifact.
-   */
-  private Artifact manifestFile(String name, String... lines) throws Exception {
-    Artifact artifact = getBinArtifactWithNoOwner(name);
-    scratch.file(artifact.getPath().getPathString(), lines);
-    return artifact;
-  }
-
   private String computeKey(SourceManifestAction action) {
     Fingerprint fp = new Fingerprint();
     action.computeKey(actionKeyContext, fp);
diff --git a/src/test/java/com/google/devtools/build/lib/packages/BUILD b/src/test/java/com/google/devtools/build/lib/packages/BUILD
index ec361e4..7382797 100644
--- a/src/test/java/com/google/devtools/build/lib/packages/BUILD
+++ b/src/test/java/com/google/devtools/build/lib/packages/BUILD
@@ -132,6 +132,7 @@
         "//src/main/java/com/google/devtools/build/lib/analysis:analysis_cluster",
         "//src/main/java/com/google/devtools/build/lib/analysis:blaze_directories",
         "//src/main/java/com/google/devtools/build/lib/analysis:server_directories",
+        "//src/main/java/com/google/devtools/build/lib/bazel/rules/python",
         "//src/main/java/com/google/devtools/build/lib/clock",
         "//src/main/java/com/google/devtools/build/lib/cmdline",
         "//src/main/java/com/google/devtools/build/lib/events",
@@ -142,6 +143,7 @@
         "//src/main/java/com/google/devtools/build/lib/rules:repository/repository_function",
         "//src/main/java/com/google/devtools/build/lib/rules/cpp",
         "//src/main/java/com/google/devtools/build/lib/rules/proto",
+        "//src/main/java/com/google/devtools/build/lib/rules/python",
         "//src/main/java/com/google/devtools/build/lib/runtime/commands",
         "//src/main/java/com/google/devtools/build/lib/skyframe:precomputed_value",
         "//src/main/java/com/google/devtools/build/lib/skyframe:skyframe_cluster",
diff --git a/src/test/java/com/google/devtools/build/lib/packages/util/BazelMockPythonSupport.java b/src/test/java/com/google/devtools/build/lib/packages/util/BazelMockPythonSupport.java
index d96b9f2..3339c70 100644
--- a/src/test/java/com/google/devtools/build/lib/packages/util/BazelMockPythonSupport.java
+++ b/src/test/java/com/google/devtools/build/lib/packages/util/BazelMockPythonSupport.java
@@ -14,6 +14,8 @@
 
 package com.google.devtools.build.lib.packages.util;
 
+import com.google.devtools.build.lib.bazel.rules.python.BazelPythonSemantics;
+import com.google.devtools.build.lib.rules.python.PythonSemantics;
 import com.google.devtools.build.lib.testutil.TestConstants;
 import java.io.IOException;
 
@@ -91,4 +93,9 @@
     // Under BazelPythonSemantics, we can simply set --python_top to be the py_runtime target.
     return pyRuntimeLabel;
   }
+
+  @Override
+  public PythonSemantics getPythonSemantics() {
+    return new BazelPythonSemantics();
+  }
 }
diff --git a/src/test/java/com/google/devtools/build/lib/packages/util/MockPythonSupport.java b/src/test/java/com/google/devtools/build/lib/packages/util/MockPythonSupport.java
index 794ee28..7d1c973 100644
--- a/src/test/java/com/google/devtools/build/lib/packages/util/MockPythonSupport.java
+++ b/src/test/java/com/google/devtools/build/lib/packages/util/MockPythonSupport.java
@@ -17,6 +17,7 @@
 import com.google.common.base.Joiner;
 import com.google.common.base.Preconditions;
 import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.lib.rules.python.PythonSemantics;
 import com.google.devtools.build.lib.testutil.TestConstants;
 import java.io.IOException;
 import java.util.List;
@@ -37,6 +38,8 @@
   public abstract String createPythonTopEntryPoint(MockToolsConfig config, String pyRuntimeLabel)
       throws IOException;
 
+  public abstract PythonSemantics getPythonSemantics();
+
   /**
    * Defines a file simulating the part of @rules_python//python:defs.bzl that defines macros for
    * native rules.