pybinaryguard 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pybinaryguard/__init__.py +78 -0
- pybinaryguard/__main__.py +7 -0
- pybinaryguard/_compat/__init__.py +0 -0
- pybinaryguard/agent/__init__.py +63 -0
- pybinaryguard/agent/guard.py +232 -0
- pybinaryguard/agent/recommender.py +283 -0
- pybinaryguard/agent/schema.py +200 -0
- pybinaryguard/agent/simulator.py +430 -0
- pybinaryguard/agent/tool_interface.py +474 -0
- pybinaryguard/analyzers/__init__.py +209 -0
- pybinaryguard/analyzers/base.py +40 -0
- pybinaryguard/analyzers/dependency_analyzer.py +336 -0
- pybinaryguard/analyzers/elf_analyzer.py +754 -0
- pybinaryguard/analyzers/symbol_analyzer.py +280 -0
- pybinaryguard/analyzers/wheel_analyzer.py +308 -0
- pybinaryguard/cli/__init__.py +5 -0
- pybinaryguard/cli/commands.py +414 -0
- pybinaryguard/cli/formatters.py +720 -0
- pybinaryguard/cli/main.py +250 -0
- pybinaryguard/diagnostics/__init__.py +13 -0
- pybinaryguard/diagnostics/explainer.py +356 -0
- pybinaryguard/diagnostics/findings.py +146 -0
- pybinaryguard/diagnostics/suggestions.py +508 -0
- pybinaryguard/frameworks/__init__.py +20 -0
- pybinaryguard/frameworks/onnxruntime.py +214 -0
- pybinaryguard/frameworks/pytorch.py +223 -0
- pybinaryguard/frameworks/tensorflow.py +266 -0
- pybinaryguard/frameworks/tensorrt.py +189 -0
- pybinaryguard/models/__init__.py +19 -0
- pybinaryguard/models/enums.py +102 -0
- pybinaryguard/models/finding.py +109 -0
- pybinaryguard/models/package.py +121 -0
- pybinaryguard/models/system.py +118 -0
- pybinaryguard/plugins/__init__.py +19 -0
- pybinaryguard/plugins/contrib/__init__.py +7 -0
- pybinaryguard/plugins/contrib/gstreamer.py +109 -0
- pybinaryguard/plugins/contrib/jetson.py +385 -0
- pybinaryguard/plugins/contrib/opencv.py +306 -0
- pybinaryguard/plugins/contrib/tensorrt.py +426 -0
- pybinaryguard/plugins/hooks.py +273 -0
- pybinaryguard/plugins/loader.py +190 -0
- pybinaryguard/predictor/__init__.py +23 -0
- pybinaryguard/predictor/dependency_graph.py +226 -0
- pybinaryguard/predictor/linker_simulator.py +161 -0
- pybinaryguard/predictor/predictor.py +144 -0
- pybinaryguard/predictor/resolver.py +282 -0
- pybinaryguard/probes/__init__.py +73 -0
- pybinaryguard/probes/base.py +45 -0
- pybinaryguard/probes/board_probe.py +248 -0
- pybinaryguard/probes/cpu_probe.py +176 -0
- pybinaryguard/probes/glibc_probe.py +215 -0
- pybinaryguard/probes/gpu_probe.py +430 -0
- pybinaryguard/probes/library_probe.py +132 -0
- pybinaryguard/probes/os_probe.py +227 -0
- pybinaryguard/probes/python_probe.py +135 -0
- pybinaryguard/probes/toolchain_probe.py +89 -0
- pybinaryguard/probes/venv_probe.py +111 -0
- pybinaryguard/profiles/__init__.py +12 -0
- pybinaryguard/profiles/engine.py +343 -0
- pybinaryguard/rules/__init__.py +24 -0
- pybinaryguard/rules/base.py +57 -0
- pybinaryguard/rules/builtin/__init__.py +136 -0
- pybinaryguard/rules/builtin/arch_rules.py +86 -0
- pybinaryguard/rules/builtin/board_profile_rules.py +282 -0
- pybinaryguard/rules/builtin/container_rules.py +170 -0
- pybinaryguard/rules/builtin/cpu_rules.py +204 -0
- pybinaryguard/rules/builtin/cuda_rules.py +760 -0
- pybinaryguard/rules/builtin/dependency_rules.py +278 -0
- pybinaryguard/rules/builtin/framework_rules.py +252 -0
- pybinaryguard/rules/builtin/glibc_rules.py +320 -0
- pybinaryguard/rules/builtin/numpy_rules.py +110 -0
- pybinaryguard/rules/builtin/predictive_rules.py +165 -0
- pybinaryguard/rules/builtin/python_abi_rules.py +259 -0
- pybinaryguard/rules/builtin/source_build_rules.py +150 -0
- pybinaryguard/rules/builtin/venv_rules.py +159 -0
- pybinaryguard/rules/engine.py +123 -0
- pybinaryguard/scanner.py +904 -0
- pybinaryguard/scoring/__init__.py +19 -0
- pybinaryguard/scoring/engine.py +416 -0
- pybinaryguard/snapshot/__init__.py +20 -0
- pybinaryguard/snapshot/generator.py +168 -0
- pybinaryguard/snapshot/lockfile.py +152 -0
- pybinaryguard/snapshot/verifier.py +315 -0
- pybinaryguard/validators/__init__.py +7 -0
- pybinaryguard/validators/import_validator.py +231 -0
- pybinaryguard-1.0.0.dist-info/METADATA +876 -0
- pybinaryguard-1.0.0.dist-info/RECORD +91 -0
- pybinaryguard-1.0.0.dist-info/WHEEL +5 -0
- pybinaryguard-1.0.0.dist-info/entry_points.txt +8 -0
- pybinaryguard-1.0.0.dist-info/licenses/LICENSE +21 -0
- pybinaryguard-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""Abstract base class for all binary analyzers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from abc import ABC, abstractmethod
|
|
6
|
+
|
|
7
|
+
from pybinaryguard.models.package import PackageBinaryInfo
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class AnalyzerBase(ABC):
|
|
11
|
+
"""Base class for binary analyzers.
|
|
12
|
+
|
|
13
|
+
Every analyzer must:
|
|
14
|
+
- Have a unique ``name`` attribute used for identification and logging.
|
|
15
|
+
- Implement ``analyze()`` which enriches a ``PackageBinaryInfo`` in place
|
|
16
|
+
and returns it.
|
|
17
|
+
- Be completely read-only -- analyzers must never modify files on disk.
|
|
18
|
+
- Handle all internal I/O errors gracefully rather than raising.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
name: str # Analyzer identifier -- must be set by subclasses
|
|
22
|
+
|
|
23
|
+
@abstractmethod
|
|
24
|
+
def analyze(self, package_info: PackageBinaryInfo) -> PackageBinaryInfo:
|
|
25
|
+
"""Analyze and enrich the *package_info* with findings.
|
|
26
|
+
|
|
27
|
+
Parameters
|
|
28
|
+
----------
|
|
29
|
+
package_info:
|
|
30
|
+
The package descriptor to populate. The analyzer reads
|
|
31
|
+
binary files referenced by the descriptor, extracts metadata,
|
|
32
|
+
and mutates the descriptor's fields accordingly.
|
|
33
|
+
|
|
34
|
+
Returns
|
|
35
|
+
-------
|
|
36
|
+
PackageBinaryInfo
|
|
37
|
+
The same *package_info* instance (mutated in place) so callers
|
|
38
|
+
can chain analyzers conveniently.
|
|
39
|
+
"""
|
|
40
|
+
...
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
"""Dependency analyzer -- resolves DT_NEEDED chains statically.
|
|
2
|
+
|
|
3
|
+
For each shared library required by a package's ``.so`` files, this
|
|
4
|
+
analyzer checks whether the library can be found at runtime without
|
|
5
|
+
executing any binaries. Libraries that cannot be resolved are added to
|
|
6
|
+
``PackageBinaryInfo.missing_libraries``.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import logging
|
|
12
|
+
import os
|
|
13
|
+
import platform
|
|
14
|
+
from typing import Dict, List, Optional, Set, Tuple
|
|
15
|
+
|
|
16
|
+
from pybinaryguard.analyzers.base import AnalyzerBase
|
|
17
|
+
from pybinaryguard.models.package import PackageBinaryInfo
|
|
18
|
+
from pybinaryguard.models.system import SystemProfile
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
# Standard library search paths per architecture
|
|
23
|
+
_STANDARD_LIB_PATHS: Tuple[str, ...] = (
|
|
24
|
+
"/lib",
|
|
25
|
+
"/lib64",
|
|
26
|
+
"/usr/lib",
|
|
27
|
+
"/usr/lib64",
|
|
28
|
+
"/usr/local/lib",
|
|
29
|
+
"/usr/local/lib64",
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
# Architecture-specific multilib directories
|
|
33
|
+
_ARCH_LIB_DIRS: Dict[str, Tuple[str, ...]] = {
|
|
34
|
+
"x86_64": (
|
|
35
|
+
"/lib/x86_64-linux-gnu",
|
|
36
|
+
"/usr/lib/x86_64-linux-gnu",
|
|
37
|
+
),
|
|
38
|
+
"aarch64": (
|
|
39
|
+
"/lib/aarch64-linux-gnu",
|
|
40
|
+
"/usr/lib/aarch64-linux-gnu",
|
|
41
|
+
),
|
|
42
|
+
"armv7l": (
|
|
43
|
+
"/lib/arm-linux-gnueabihf",
|
|
44
|
+
"/usr/lib/arm-linux-gnueabihf",
|
|
45
|
+
),
|
|
46
|
+
"i686": (
|
|
47
|
+
"/lib/i386-linux-gnu",
|
|
48
|
+
"/usr/lib/i386-linux-gnu",
|
|
49
|
+
"/lib32",
|
|
50
|
+
"/usr/lib32",
|
|
51
|
+
),
|
|
52
|
+
"ppc64le": (
|
|
53
|
+
"/lib/powerpc64le-linux-gnu",
|
|
54
|
+
"/usr/lib/powerpc64le-linux-gnu",
|
|
55
|
+
),
|
|
56
|
+
"s390x": (
|
|
57
|
+
"/lib/s390x-linux-gnu",
|
|
58
|
+
"/usr/lib/s390x-linux-gnu",
|
|
59
|
+
),
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
# Libraries that are always expected to be provided by the C runtime and
|
|
63
|
+
# should not be flagged as missing.
|
|
64
|
+
_ALWAYS_PRESENT: frozenset = frozenset({
|
|
65
|
+
"linux-vdso.so.1",
|
|
66
|
+
"linux-gate.so.1",
|
|
67
|
+
"linux-vdso64.so.1",
|
|
68
|
+
"ld-linux-x86-64.so.2",
|
|
69
|
+
"ld-linux.so.2",
|
|
70
|
+
"ld-linux-aarch64.so.1",
|
|
71
|
+
"ld-linux-armhf.so.3",
|
|
72
|
+
"ld-linux-riscv64-lp64d.so.1",
|
|
73
|
+
"ld64.so.1",
|
|
74
|
+
"ld64.so.2",
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class DependencyAnalyzer(AnalyzerBase):
|
|
79
|
+
"""Resolves DT_NEEDED library dependencies through static path searching.
|
|
80
|
+
|
|
81
|
+
For each library referenced by a package's shared objects, the analyzer
|
|
82
|
+
searches (in order):
|
|
83
|
+
|
|
84
|
+
1. ``DT_RPATH`` / ``DT_RUNPATH`` from the binary itself
|
|
85
|
+
2. ``LD_LIBRARY_PATH`` from the system profile
|
|
86
|
+
3. The ldconfig cache (``SystemProfile.ldconfig_cache``)
|
|
87
|
+
4. Standard library directories (``/lib``, ``/usr/lib``, arch-specific)
|
|
88
|
+
|
|
89
|
+
Libraries that cannot be found are added to
|
|
90
|
+
``PackageBinaryInfo.missing_libraries``.
|
|
91
|
+
|
|
92
|
+
This analyzer never executes any binary (no ``ldd``, no ``ldconfig``).
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
name: str = "dependency"
|
|
96
|
+
|
|
97
|
+
def __init__(self, system_profile: Optional[SystemProfile] = None) -> None:
|
|
98
|
+
self._profile = system_profile
|
|
99
|
+
|
|
100
|
+
def analyze(self, package_info: PackageBinaryInfo) -> PackageBinaryInfo:
|
|
101
|
+
"""Resolve library dependencies and flag missing ones.
|
|
102
|
+
|
|
103
|
+
Parameters
|
|
104
|
+
----------
|
|
105
|
+
package_info:
|
|
106
|
+
The package descriptor whose ``shared_objects`` list has
|
|
107
|
+
already been populated by the ELF analyzer.
|
|
108
|
+
|
|
109
|
+
Returns
|
|
110
|
+
-------
|
|
111
|
+
PackageBinaryInfo
|
|
112
|
+
The same (mutated) instance.
|
|
113
|
+
"""
|
|
114
|
+
if not package_info.shared_objects:
|
|
115
|
+
return package_info
|
|
116
|
+
|
|
117
|
+
# Build the set of all DT_NEEDED libraries across the package
|
|
118
|
+
all_needed: Set[str] = set()
|
|
119
|
+
for so in package_info.shared_objects:
|
|
120
|
+
for lib in so.dt_needed:
|
|
121
|
+
all_needed.add(lib)
|
|
122
|
+
|
|
123
|
+
if not all_needed:
|
|
124
|
+
return package_info
|
|
125
|
+
|
|
126
|
+
# Collect search paths from system profile
|
|
127
|
+
ld_library_paths = self._get_ld_library_path()
|
|
128
|
+
ldconfig_cache = self._get_ldconfig_cache()
|
|
129
|
+
arch_dirs = self._get_arch_lib_dirs()
|
|
130
|
+
|
|
131
|
+
# Collect per-binary RPATH/RUNPATH
|
|
132
|
+
rpath_dirs: List[str] = []
|
|
133
|
+
runpath_dirs: List[str] = []
|
|
134
|
+
for so in package_info.shared_objects:
|
|
135
|
+
if so.dt_rpath:
|
|
136
|
+
for p in so.dt_rpath.split(":"):
|
|
137
|
+
resolved = self._resolve_rpath_token(p, so.path)
|
|
138
|
+
if resolved and resolved not in rpath_dirs:
|
|
139
|
+
rpath_dirs.append(resolved)
|
|
140
|
+
if so.dt_runpath:
|
|
141
|
+
for p in so.dt_runpath.split(":"):
|
|
142
|
+
resolved = self._resolve_rpath_token(p, so.path)
|
|
143
|
+
if resolved and resolved not in runpath_dirs:
|
|
144
|
+
runpath_dirs.append(resolved)
|
|
145
|
+
|
|
146
|
+
# Also include the package's own install path as a search location
|
|
147
|
+
# (many packages bundle their own .so files alongside each other)
|
|
148
|
+
package_lib_dirs: List[str] = []
|
|
149
|
+
if os.path.isdir(package_info.install_path):
|
|
150
|
+
package_lib_dirs.append(package_info.install_path)
|
|
151
|
+
# Also check common subdirectories
|
|
152
|
+
for subdir in ("lib", "libs", ".libs"):
|
|
153
|
+
candidate = os.path.join(package_info.install_path, subdir)
|
|
154
|
+
if os.path.isdir(candidate):
|
|
155
|
+
package_lib_dirs.append(candidate)
|
|
156
|
+
|
|
157
|
+
# Build the names of libraries that the package itself provides
|
|
158
|
+
# (so intra-package deps don't get flagged as missing)
|
|
159
|
+
provided_by_package: Set[str] = set()
|
|
160
|
+
for so in package_info.shared_objects:
|
|
161
|
+
provided_by_package.add(so.filename)
|
|
162
|
+
if so.dt_soname:
|
|
163
|
+
provided_by_package.add(so.dt_soname)
|
|
164
|
+
|
|
165
|
+
# Resolve each library
|
|
166
|
+
missing: Set[str] = set()
|
|
167
|
+
for lib_name in sorted(all_needed):
|
|
168
|
+
if lib_name in _ALWAYS_PRESENT:
|
|
169
|
+
continue
|
|
170
|
+
if lib_name in provided_by_package:
|
|
171
|
+
continue
|
|
172
|
+
if not self._find_library(
|
|
173
|
+
lib_name,
|
|
174
|
+
rpath_dirs=rpath_dirs,
|
|
175
|
+
runpath_dirs=runpath_dirs,
|
|
176
|
+
ld_library_paths=ld_library_paths,
|
|
177
|
+
ldconfig_cache=ldconfig_cache,
|
|
178
|
+
package_dirs=package_lib_dirs,
|
|
179
|
+
arch_dirs=arch_dirs,
|
|
180
|
+
):
|
|
181
|
+
missing.add(lib_name)
|
|
182
|
+
|
|
183
|
+
package_info.missing_libraries = missing
|
|
184
|
+
return package_info
|
|
185
|
+
|
|
186
|
+
# ------------------------------------------------------------------
|
|
187
|
+
# Library resolution
|
|
188
|
+
# ------------------------------------------------------------------
|
|
189
|
+
|
|
190
|
+
@staticmethod
|
|
191
|
+
def _find_library(
|
|
192
|
+
name: str,
|
|
193
|
+
*,
|
|
194
|
+
rpath_dirs: List[str],
|
|
195
|
+
runpath_dirs: List[str],
|
|
196
|
+
ld_library_paths: List[str],
|
|
197
|
+
ldconfig_cache: Dict[str, str],
|
|
198
|
+
package_dirs: List[str],
|
|
199
|
+
arch_dirs: List[str],
|
|
200
|
+
) -> bool:
|
|
201
|
+
"""Search for a library by name using the ELF resolution order.
|
|
202
|
+
|
|
203
|
+
The search order mirrors the dynamic linker (ld.so) behavior:
|
|
204
|
+
|
|
205
|
+
1. DT_RPATH (deprecated but still used)
|
|
206
|
+
2. LD_LIBRARY_PATH
|
|
207
|
+
3. DT_RUNPATH
|
|
208
|
+
4. ldconfig cache
|
|
209
|
+
5. Package-internal directories
|
|
210
|
+
6. Standard and architecture-specific paths
|
|
211
|
+
|
|
212
|
+
Parameters
|
|
213
|
+
----------
|
|
214
|
+
name:
|
|
215
|
+
The library file name (e.g. ``libz.so.1``).
|
|
216
|
+
|
|
217
|
+
Returns
|
|
218
|
+
-------
|
|
219
|
+
bool
|
|
220
|
+
``True`` if the library was found.
|
|
221
|
+
"""
|
|
222
|
+
# 1. RPATH
|
|
223
|
+
for d in rpath_dirs:
|
|
224
|
+
if _file_exists(os.path.join(d, name)):
|
|
225
|
+
return True
|
|
226
|
+
|
|
227
|
+
# 2. LD_LIBRARY_PATH
|
|
228
|
+
for d in ld_library_paths:
|
|
229
|
+
if _file_exists(os.path.join(d, name)):
|
|
230
|
+
return True
|
|
231
|
+
|
|
232
|
+
# 3. RUNPATH
|
|
233
|
+
for d in runpath_dirs:
|
|
234
|
+
if _file_exists(os.path.join(d, name)):
|
|
235
|
+
return True
|
|
236
|
+
|
|
237
|
+
# 4. ldconfig cache
|
|
238
|
+
if name in ldconfig_cache:
|
|
239
|
+
path = ldconfig_cache[name]
|
|
240
|
+
if _file_exists(path):
|
|
241
|
+
return True
|
|
242
|
+
|
|
243
|
+
# 5. Package-internal directories
|
|
244
|
+
for d in package_dirs:
|
|
245
|
+
if _file_exists(os.path.join(d, name)):
|
|
246
|
+
return True
|
|
247
|
+
|
|
248
|
+
# 6. Standard + arch-specific paths
|
|
249
|
+
for d in list(_STANDARD_LIB_PATHS) + arch_dirs:
|
|
250
|
+
if _file_exists(os.path.join(d, name)):
|
|
251
|
+
return True
|
|
252
|
+
|
|
253
|
+
return False
|
|
254
|
+
|
|
255
|
+
# ------------------------------------------------------------------
|
|
256
|
+
# RPATH token resolution
|
|
257
|
+
# ------------------------------------------------------------------
|
|
258
|
+
|
|
259
|
+
@staticmethod
|
|
260
|
+
def _resolve_rpath_token(path: str, binary_path: str) -> Optional[str]:
|
|
261
|
+
"""Resolve ``$ORIGIN`` and ``${ORIGIN}`` tokens in RPATH/RUNPATH.
|
|
262
|
+
|
|
263
|
+
Parameters
|
|
264
|
+
----------
|
|
265
|
+
path:
|
|
266
|
+
A single path component from RPATH/RUNPATH.
|
|
267
|
+
binary_path:
|
|
268
|
+
The absolute path to the binary that contains the RPATH.
|
|
269
|
+
|
|
270
|
+
Returns
|
|
271
|
+
-------
|
|
272
|
+
str or None
|
|
273
|
+
The resolved absolute path, or ``None`` if invalid.
|
|
274
|
+
"""
|
|
275
|
+
if not path:
|
|
276
|
+
return None
|
|
277
|
+
|
|
278
|
+
origin = os.path.dirname(os.path.abspath(binary_path))
|
|
279
|
+
resolved = path.replace("$ORIGIN", origin).replace("${ORIGIN}", origin)
|
|
280
|
+
resolved = os.path.normpath(resolved)
|
|
281
|
+
|
|
282
|
+
if os.path.isdir(resolved):
|
|
283
|
+
return resolved
|
|
284
|
+
|
|
285
|
+
# The directory might not exist yet on the system being analyzed,
|
|
286
|
+
# but we still return it so the caller can check
|
|
287
|
+
return resolved
|
|
288
|
+
|
|
289
|
+
# ------------------------------------------------------------------
|
|
290
|
+
# Profile helpers
|
|
291
|
+
# ------------------------------------------------------------------
|
|
292
|
+
|
|
293
|
+
def _get_ld_library_path(self) -> List[str]:
|
|
294
|
+
"""Return LD_LIBRARY_PATH entries from the system profile or env."""
|
|
295
|
+
if self._profile is not None and self._profile.ld_library_path:
|
|
296
|
+
return list(self._profile.ld_library_path)
|
|
297
|
+
|
|
298
|
+
# Fallback: read from the current environment
|
|
299
|
+
env_val = os.environ.get("LD_LIBRARY_PATH", "")
|
|
300
|
+
if env_val:
|
|
301
|
+
return [p for p in env_val.split(":") if p]
|
|
302
|
+
return []
|
|
303
|
+
|
|
304
|
+
def _get_ldconfig_cache(self) -> Dict[str, str]:
|
|
305
|
+
"""Return the ldconfig cache mapping from the system profile."""
|
|
306
|
+
if self._profile is not None:
|
|
307
|
+
return dict(self._profile.ldconfig_cache)
|
|
308
|
+
return {}
|
|
309
|
+
|
|
310
|
+
def _get_arch_lib_dirs(self) -> List[str]:
|
|
311
|
+
"""Return architecture-specific library directories."""
|
|
312
|
+
arch = ""
|
|
313
|
+
if self._profile is not None:
|
|
314
|
+
arch = self._profile.architecture.value
|
|
315
|
+
else:
|
|
316
|
+
arch = platform.machine()
|
|
317
|
+
|
|
318
|
+
dirs = list(_ARCH_LIB_DIRS.get(arch, ()))
|
|
319
|
+
|
|
320
|
+
# Also check the current machine if different
|
|
321
|
+
if not dirs:
|
|
322
|
+
machine = platform.machine()
|
|
323
|
+
dirs = list(_ARCH_LIB_DIRS.get(machine, ()))
|
|
324
|
+
|
|
325
|
+
return dirs
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def _file_exists(path: str) -> bool:
|
|
329
|
+
"""Check if *path* exists and is a file (or symlink to a file).
|
|
330
|
+
|
|
331
|
+
Silently returns ``False`` on permission errors.
|
|
332
|
+
"""
|
|
333
|
+
try:
|
|
334
|
+
return os.path.isfile(path)
|
|
335
|
+
except OSError:
|
|
336
|
+
return False
|