Repository: pypa/setuptools_scm

Issue:
hg failing if external repo is broken
I have /.hg in my root fs of Gentoo Linux, tracking almost /etc and other files. And if something is broken with that external repo, all Python installations by the package manager inside of it's building sandbox in /var/tmp/portage are failing like this:
```
writing dependency_links to python_mimeparse.egg-info/dependency_links.txt
abort: No such file or directory: '/boot/config'
Traceback (most recent call last):
  File "setup.py", line 50, in <module>
    long_description=read('README.rst')
  File "/usr/lib64/python2.7/site-packages/setuptools/__init__.py", line 145, in setup
    return distutils.core.setup(**attrs)
  File "/usr/lib64/python2.7/distutils/core.py", line 151, in setup
    dist.run_commands()
  File "/usr/lib64/python2.7/distutils/dist.py", line 953, in run_commands
    self.run_command(cmd)
  File "/usr/lib64/python2.7/distutils/dist.py", line 972, in run_command
    cmd_obj.run()
  File "/usr/lib64/python2.7/site-packages/setuptools/command/install.py", line 61, in run
    return orig.install.run(self)
  File "/usr/lib64/python2.7/distutils/command/install.py", line 575, in run
    self.run_command(cmd_name)
  File "/usr/lib64/python2.7/distutils/cmd.py", line 326, in run_command
    self.distribution.run_command(command)
  File "/usr/lib64/python2.7/distutils/dist.py", line 972, in run_command
    cmd_obj.run()
  File "/usr/lib64/python2.7/site-packages/setuptools/command/install_egg_info.py", line 34, in run
    self.run_command('egg_info')
  File "/usr/lib64/python2.7/distutils/cmd.py", line 326, in run_command
    self.distribution.run_command(command)
  File "/usr/lib64/python2.7/distutils/dist.py", line 972, in run_command
    cmd_obj.run()
  File "/usr/lib64/python2.7/site-packages/setuptools/command/egg_info.py", line 296, in run
    self.find_sources()
  File "/usr/lib64/python2.7/site-packages/setuptools/command/egg_info.py", line 303, in find_sources
    mm.run()
  File "/usr/lib64/python2.7/site-packages/setuptools/command/egg_info.py", line 534, in run
    self.add_defaults()
  File "/usr/lib64/python2.7/site-packages/setuptools/command/egg_info.py", line 574, in add_defaults
    rcfiles = list(walk_revctrl())
  File "/usr/lib64/python2.7/site-packages/setuptools/command/sdist.py", line 20, in walk_revctrl
    for item in ep.load()(dirname):
  File "/usr/lib64/python2.7/site-packages/setuptools_scm/integration.py", line 27, in find_files
    res = command(path)
  File "/usr/lib64/python2.7/site-packages/setuptools_scm/file_finder_hg.py", line 46, in hg_find_files
    hg_files, hg_dirs = _hg_ls_files_and_dirs(toplevel)
  File "/usr/lib64/python2.7/site-packages/setuptools_scm/file_finder_hg.py", line 29, in _hg_ls_files_and_dirs
    ["hg", "files"], cwd=toplevel, universal_newlines=True
  File "/usr/lib64/python2.7/subprocess.py", line 223, in check_output
    raise CalledProcessError(retcode, cmd, output=output)
subprocess.CalledProcessError: Command '['hg', 'files']' returned non-zero exit status 255
```

setuptools should be completely independent from outside Mercurial repos or maybe have an option to ignore that like this:

```patch
--- a/src/setuptools_scm/file_finder_hg.py      2019-08-01 08:24:15.060553831 +0200
+++ b/src/setuptools_scm/file_finder_hg.py      2019-08-01 08:26:11.334617120 +0200
@@ -41,7 +41,11 @@
 
 def hg_find_files(path=""):
     toplevel = _hg_toplevel(path)
     if not toplevel:
         return []
+    # SETUPTOOLS_SCM_IGNORE_VCS_ROOTS is a ':'-separated list of VCS roots to ignore.
+    # Example: SETUPTOOLS_SCM_IGNORE_VCS_ROOTS=/var:/etc:/home/me
+    if toplevel in os.environ.get('SETUPTOOLS_SCM_IGNORE_VCS_ROOTS', '').split(':'):
+        return []
     hg_files, hg_dirs = _hg_ls_files_and_dirs(toplevel)
     return scm_find_files(path, hg_files, hg_dirs)
```

Discussion:

Patch:
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index c376a55..34f923e 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -1,6 +1,7 @@
 v4.2.0
 ======
 
+* fix #352: add support for generally ignoring specific vcs roots
 * fix #471: better error for version bump failing on complex but accepted tag
 * fix #479: raise indicative error when tags carry non-parsable information
 * Add `no-guess-dev` which does no next version guessing, just adds `.post1.devN` in
diff --git a/README.rst b/README.rst
index a318332..d4b2453 100644
--- a/README.rst
+++ b/README.rst
@@ -446,6 +446,11 @@ Environment variables
     derived, otherwise the current time is used
     (https://reproducible-builds.org/docs/source-date-epoch/)
 
+
+:SETUPTOOLS_SCM_IGNORE_VCS_ROOTS:
+    when defined, a ``os.pathsep`` separated list
+    of directory names to ignore for root finding
+
 Extending setuptools_scm
 ------------------------
 
diff --git a/src/setuptools_scm/file_finder.py b/src/setuptools_scm/file_finder.py
index 77ec146..5b85117 100644
--- a/src/setuptools_scm/file_finder.py
+++ b/src/setuptools_scm/file_finder.py
@@ -1,4 +1,5 @@
 import os
+from .utils import trace
 
 
 def scm_find_files(path, scm_files, scm_dirs):
@@ -53,3 +54,16 @@ def scm_find_files(path, scm_files, scm_dirs):
                 res.append(os.path.join(path, os.path.relpath(fullfilename, realpath)))
         seen.add(realdirpath)
     return res
+
+
+def is_toplevel_acceptable(toplevel):
+    ""
+    if toplevel is None:
+        return False
+
+    ignored = os.environ.get("SETUPTOOLS_SCM_IGNORE_VCS_ROOTS", "").split(os.pathsep)
+    ignored = [os.path.normcase(p) for p in ignored]
+
+    trace(toplevel, ignored)
+
+    return toplevel not in ignored
diff --git a/src/setuptools_scm/file_finder_git.py b/src/setuptools_scm/file_finder_git.py
index 9aa6245..1d3e69b 100644
--- a/src/setuptools_scm/file_finder_git.py
+++ b/src/setuptools_scm/file_finder_git.py
@@ -3,6 +3,7 @@ import subprocess
 import tarfile
 import logging
 from .file_finder import scm_find_files
+from .file_finder import is_toplevel_acceptable
 from .utils import trace
 
 log = logging.getLogger(__name__)
@@ -60,7 +61,7 @@ def _git_ls_files_and_dirs(toplevel):
 
 def git_find_files(path=""):
     toplevel = _git_toplevel(path)
-    if not toplevel:
+    if not is_toplevel_acceptable(toplevel):
         return []
     fullpath = os.path.abspath(os.path.normpath(path))
     if not fullpath.startswith(toplevel):
diff --git a/src/setuptools_scm/file_finder_hg.py b/src/setuptools_scm/file_finder_hg.py
index 2aa1e16..816560d 100644
--- a/src/setuptools_scm/file_finder_hg.py
+++ b/src/setuptools_scm/file_finder_hg.py
@@ -2,6 +2,7 @@ import os
 import subprocess
 
 from .file_finder import scm_find_files
+from .file_finder import is_toplevel_acceptable
 
 
 def _hg_toplevel(path):
@@ -41,7 +42,7 @@ def _hg_ls_files_and_dirs(toplevel):
 
 def hg_find_files(path=""):
     toplevel = _hg_toplevel(path)
-    if not toplevel:
+    if not is_toplevel_acceptable(toplevel):
         return []
     hg_files, hg_dirs = _hg_ls_files_and_dirs(toplevel)
     return scm_find_files(path, hg_files, hg_dirs)


Test patch:
diff --git a/testing/test_file_finder.py b/testing/test_file_finder.py
index 463d3d4..55b9cea 100644
--- a/testing/test_file_finder.py
+++ b/testing/test_file_finder.py
@@ -126,6 +126,12 @@ def test_symlink_file_out_of_git(inwd):
     assert set(find_files("adir")) == _sep({"adir/filea"})
 
 
+@pytest.mark.parametrize("path_add", ["{cwd}", "{cwd}" + os.pathsep + "broken"])
+def test_ignore_root(inwd, monkeypatch, path_add):
+    monkeypatch.setenv("SETUPTOOLS_SCM_IGNORE_VCS_ROOTS", path_add.format(cwd=inwd.cwd))
+    assert find_files() == []
+
+
 def test_empty_root(inwd):
     subdir = inwd.cwd / "cdir" / "subdir"
     subdir.mkdir(parents=True)