forked from tianocore/edk2-pytool-library
-
Notifications
You must be signed in to change notification settings - Fork 0
/
path_utilities.py
372 lines (313 loc) · 15.9 KB
/
path_utilities.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
# @file path_utilities.py
# Code to help convert Edk2, absolute, and relative file paths
#
# Copyright (c) Microsoft Corporation
#
# SPDX-License-Identifier: BSD-2-Clause-Patent
##
"""Code to help convert Edk2, absolute, and relative file paths."""
import os
import logging
import fnmatch
import errno
from typing import Iterable
from pathlib import Path
class Edk2Path(object):
"""Represents edk2 file paths.
Class that helps perform path operations within an EDK workspace.
"""
def __init__(self, ws: os.PathLike, package_path_list: Iterable[os.PathLike],
error_on_invalid_pp: bool = True):
"""Constructor.
Args:
ws (os.PathLike): absolute path or cwd relative path of the workspace.
package_path_list (Iterable[os.PathLike]): list of packages path.
Entries can be Absolute path, workspace relative path, or CWD relative.
error_on_invalid_pp (bool): default value is True. If packages path
value is invalid raise exception.
Raises:
(NotADirectoryError): Invalid workspace or package path directory.
"""
self.WorkspacePath = ws
self.logger = logging.getLogger("Edk2Path")
# Other code is dependent the following types, so keep it that way:
# - self.PackagePathList: List[str]
# - self.WorkspacePath: str
self.PackagePathList = []
self.WorkspacePath = ""
workspace_candidate_path = Path(ws)
if not workspace_candidate_path.is_absolute():
workspace_candidate_path = Path(os.getcwd(), ws)
if not workspace_candidate_path.is_dir():
raise NotADirectoryError(
errno.ENOENT,
os.strerror(errno.ENOENT),
workspace_candidate_path.resolve())
self.WorkspacePath = str(workspace_candidate_path)
candidate_package_path_list = []
for a in package_path_list:
if os.path.isabs(a):
candidate_package_path_list.append(Path(a))
else:
wsr = Path(self.WorkspacePath, a)
if wsr.is_dir():
candidate_package_path_list.append(wsr)
else:
# assume current working dir relative. Will catch invalid dir when checking whole list
candidate_package_path_list.append(Path(os.getcwd(), a))
error = False
for a in candidate_package_path_list[:]:
if not a.is_dir():
self.logger.log(logging.ERROR if error_on_invalid_pp else
logging.WARNING,
f"Invalid package path entry {a.resolve()}")
candidate_package_path_list.remove(a)
error = True
self.PackagePathList = [str(p) for p in candidate_package_path_list]
if error and error_on_invalid_pp:
raise NotADirectoryError(errno.ENOENT, os.strerror(errno.ENOENT),
a.resolve())
#
# Nested package check - ensure packages do not exist in a linear
# path hierarchy.
#
# 1. Build a dictionary for each package path.
# - Key = Package path
# - Value = List of packages discovered in package path
# 2. Enumerate all keys in dictionary checking if any package
# is relative (nested) to each other.
# 3. Raise an Exception if two packages are found to be nested.
#
package_path_packages = {}
for package_path in candidate_package_path_list:
package_path_packages[package_path] = \
[Path(p).parent for p in package_path.glob('**/*.dec')]
# Note: The ability to ignore this function raising an exception on
# nested packages is temporary. Do not plan on this variable
# being available long-term and try to resolve the nested
# packages problem right away.
#
# Removal is tracked in the following GitHub issue:
# https://github.com/tianocore/edk2-pytool-library/issues/200
ignore_nested_packages = False
if "PYTOOL_TEMPORARILY_IGNORE_NESTED_EDK_PACKAGES" in os.environ and \
os.environ["PYTOOL_TEMPORARILY_IGNORE_NESTED_EDK_PACKAGES"].strip().lower() == \
"true":
ignore_nested_packages = True
for package_path, packages in package_path_packages.items():
for i, package in enumerate(packages):
for j in range(i + 1, len(packages)):
comp_package = packages[j]
if (package.is_relative_to(comp_package)
or comp_package.is_relative_to(package)):
if ignore_nested_packages:
self.logger.log(
logging.WARNING,
f"Nested packages not allowed. The packages "
f"[{str(package)}] and [{str(comp_package)}] are "
f"nested.")
self.logger.log(
logging.WARNING,
"Note 1: Nested packages are being ignored right now because the "
"\"PYTOOL_TEMPORARILY_IGNORE_NESTED_EDK_PACKAGES\" environment variable "
"is set. Do not depend on this variable long-term.")
self.logger.log(
logging.WARNING,
"Note 2: Some pytool features may not work as expected with nested packages.")
else:
raise Exception(
f"Nested packages not allowed. The packages "
f"[{str(package)}] and [{str(comp_package)}] are "
f"nested. Set the \"PYTOOL_TEMPORARILY_IGNORE_NESTED_EDK_PACKAGES\" "
f"environment variable to \"true\" as a temporary workaround "
f"until you fix the packages so they are no longer nested.")
def GetEdk2RelativePathFromAbsolutePath(self, abspath):
"""Given an absolute path return a edk2 path relative to workspace or packagespath.
Note: absolute path must be in the OS specific path form
Note: the relative path will be in POSIX-like path form
Args:
abspath (os.PathLike): absolute path to a file or directory. Path must contain OS specific separator.
Returns:
(os.PathLike): POSIX-like relative path to workspace or packagespath
(None): abspath is none
(None): path is not valid
"""
if abspath is None:
return None
relpath = None
found = False
# Check if the Absolute path starts with any of the package paths. If a match is found, build the relative
# path based off that package.
#
# Sort the package paths from from longest to shortest. This handles the case where a package and a package
# path are in the same directory. See the following path_utilities_test for a detailed explanation of the
# scenario: test_get_relative_path_when_folder_is_next_to_package
for packagepath in sorted((os.path.normcase(p) for p in self.PackagePathList), reverse=True):
# If a match is found, use the original string to avoid change in case
if os.path.normcase(abspath).startswith(packagepath):
self.logger.debug("Successfully converted AbsPath to Edk2Relative Path using PackagePath")
relpath = abspath[len(packagepath):]
found = True
break
# If a match was not found, check if absolute path is based on the workspace root.
if not found and os.path.normcase(abspath).startswith(os.path.normcase(self.WorkspacePath)):
self.logger.debug("Successfully converted AbsPath to Edk2Relative Path using WorkspacePath")
relpath = abspath[len(self.WorkspacePath):]
found = True
if found:
relpath = relpath.replace(os.sep, "/").strip("/")
self.logger.debug(f'[{abspath}] -> [{relpath}]')
return relpath
# Absolute path was not in reference to a package path or the workspace root.
self.logger.error("Failed to convert AbsPath to Edk2Relative Path")
self.logger.error(f'AbsolutePath: {abspath}')
return None
def GetAbsolutePathOnThisSystemFromEdk2RelativePath(self, relpath, log_errors=True):
"""Given a edk2 relative path return an absolute path to the file in this workspace.
Args:
relpath (os.PathLike): POSIX-like path
log_errors (:obj:`bool`, optional): whether to log errors
Returns:
(os.PathLike): absolute path in the OS specific form
(None): invalid relpath
(None): Unable to get the absolute path
"""
if relpath is None:
return None
relpath = relpath.replace("/", os.sep)
abspath = os.path.join(self.WorkspacePath, relpath)
if os.path.exists(abspath):
return abspath
for a in self.PackagePathList:
abspath = os.path.join(a, relpath)
if (os.path.exists(abspath)):
return abspath
if log_errors:
self.logger.error("Failed to convert Edk2Relative Path to an Absolute Path on this system.")
self.logger.error("Relative Path: %s" % relpath)
return None
def GetContainingPackage(self, InputPath: str) -> str:
"""Find the package that contains the given path.
This isn't perfect but at least identifies the directory consistently.
Note: The inputPath must be in the OS specific path form.
Args:
InputPath (str): absolute path to a file, directory, or module.
supports both windows and linux like paths.
Returns:
(str): name of the package that the module is in.
"""
self.logger.debug("GetContainingPackage: %s" % InputPath)
# Make a list that has the path case normalized for comparison.
# Note: This only does anything on Windows
package_paths = [os.path.normcase(x) for x in self.PackagePathList]
workspace_path = os.path.normcase(self.WorkspacePath)
# 1. Handle the case that InputPath is not in the workspace tree
path_root = None
if workspace_path not in os.path.normcase(InputPath):
for p in package_paths:
if p in os.path.normcase(InputPath):
path_root = p
break
if not path_root:
return None
# 2. Determine if the path is under a package in the workspace
# Start the search within the first available directory. If provided InputPath is a directory, start there,
# else (if InputPath is a file) move to it's parent directory and start there.
if os.path.isdir(InputPath):
dirpath = str(InputPath)
else:
dirpath = os.path.dirname(InputPath)
if not path_root:
path_root = workspace_path
while path_root != os.path.normcase(dirpath):
if os.path.exists(dirpath):
for f in os.listdir(dirpath):
if fnmatch.fnmatch(f.lower(), '*.dec'):
a = os.path.basename(dirpath)
return a
dirpath = os.path.dirname(dirpath)
return None
def GetContainingModules(self, input_path: str) -> list[str]:
"""Find the list of modules (inf files) for a file path.
Note: This function only accepts absolute paths. An exception will
be raised if a non-absolute path is given.
Note: If input_path does not exist in the filesystem, this function
will try to return the likely containing module(s) but if the
entire module has been deleted, this isn't possible.
- If a .inf file is given, that file is returned.
- Otherwise, the nearest set of .inf files (in the closest parent)
will be returned in a list of file path strings.
Args:
input_path (str): Absolute path to a file, directory, or module.
Supports both Windows and Linux like paths.
Returns:
(list[str]): Absolute paths of .inf files that could be the
containing module.
"""
input_path = Path(input_path)
if not input_path.is_absolute():
# Todo: Return a more specific exception type when
# https://github.com/tianocore/edk2-pytool-library/issues/184 is
# implemented.
raise Exception("Module path must be absolute.")
package_paths = [Path(os.path.normcase(x)) for x in self.PackagePathList]
workspace_path = Path(os.path.normcase(self.WorkspacePath))
all_root_paths = package_paths + [workspace_path]
# For each root path, find the maximum allowed root in its hierarchy.
maximum_root_paths = all_root_paths
for root_path in maximum_root_paths:
for other_root_path in maximum_root_paths[:]:
if root_path == other_root_path:
continue
if root_path.is_relative_to(other_root_path):
if len(root_path.parts) > len(other_root_path.parts):
maximum_root_paths.remove(root_path)
else:
maximum_root_paths.remove(other_root_path)
# Verify the file path is within a valid workspace or package path
# directory.
for path in maximum_root_paths:
if input_path.is_relative_to(path):
break
else:
return []
modules = []
if input_path.suffix == '.inf':
# Return the file path given since it is a module .inf file
modules = [str(input_path)]
if not modules:
# Continue to ascend directories up to a maximum root path.
#
# This handles cases like:
# ModuleDir/ | ModuleDir/ | ...similarly nested files
# Module.inf | Module.inf |
# x64/ | Common/ |
# file.c | X64/ |
# | file.c |
#
# The terminating condition of the loop is when a maximum root
# path has been reached.
#
# A maximum root path represents the maximum allowed ascension
# point in the input_path directory hierarchy as sub-roots like
# a package path pointing under a workspace path are already
# accounted for during maximum root path filtering.
#
# Given a root path is either the workspace or a package path,
# neither of which are a module directory, once that point is
# reached, all possible module candidates are exhausted.
current_dir = input_path.parent
while current_dir not in maximum_root_paths:
if current_dir.is_dir():
current_dir_inf_files = \
[str(f) for f in current_dir.iterdir() if
f.is_file() and f.suffix.lower() == '.inf']
if current_dir_inf_files:
# A .inf file(s) exist in this directory.
#
# Since this is the closest parent that can be considered
# a module, return the .inf files as module candidates.
modules.extend(current_dir_inf_files)
break
current_dir = current_dir.parent
return modules