-
-
Notifications
You must be signed in to change notification settings - Fork 3k
Add several undocumented flags (--bazel, --package-root and --cache-map) in support of Bazel #4759
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 11 commits
13a52e1
4618115
d188e9f
9d32192
f1c7834
aff2f72
8048ee4
7bc3a0b
3632c55
24de3d5
ec719df
fcc9604
c62a22e
72d2657
4dd90b8
c667dfc
a43ecdc
e17a702
0f4c8af
cd10259
eade116
e7ffae4
f19892a
a0072ab
0096ee0
cf9f6e0
268b74c
dc1f262
e103d63
80daeae
b7fcc9e
363f4b6
2314b54
605b963
a89e7a4
29f3f59
052fa08
fb24a14
05a448d
8b88cd2
aa5facd
755b540
7884ed4
01f3893
1f26bdc
3d7f633
7559fc8
210de9c
15b7d87
4135e63
6cbcb08
52649f5
3bd329e
389c908
4665933
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -66,13 +66,38 @@ | |
PYTHON_EXTENSIONS = ['.pyi', '.py'] | ||
|
||
|
||
if os.altsep: | ||
SEPARATORS = frozenset([os.sep, os.altsep]) | ||
else: | ||
SEPARATORS = frozenset([os.sep]) | ||
|
||
|
||
Graph = Dict[str, 'State'] | ||
|
||
|
||
def getmtime(name: str) -> int: | ||
return int(os.path.getmtime(name)) | ||
|
||
|
||
def relpath(path: str) -> str: | ||
if not path or path[0] not in SEPARATORS: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. """Returns the path relative to the current working directory.""" |
||
# Relative path, or has drive letter prefix (C: etc.) on Windows. | ||
assert not os.path.isabs(path), "%s is neither rel nor abs" % (path,) | ||
return path | ||
if os.altsep and os.altsep in path: | ||
# So the rest of this code doesn't need to handle altsep on Windows. | ||
path = path.replace(os.altsep, os.sep) | ||
cwd = os.getcwd() | ||
for i in range(10): | ||
if path.startswith(cwd + os.sep): | ||
# The .lstrip(os.sep) call strips duplicate leading slashes. | ||
return '../' * i + path[len(cwd) + 1:].lstrip(os.sep) | ||
cwd = os.path.dirname(cwd) | ||
if all(c == os.sep for c in cwd): | ||
break | ||
return path | ||
|
||
|
||
# TODO: Get rid of BuildResult. We might as well return a BuildManager. | ||
class BuildResult: | ||
"""The result of a successful build. | ||
|
@@ -228,7 +253,12 @@ def _build(sources: List[BuildSource], | |
# to the lib_path | ||
# TODO: Don't do this in some cases; for motivation see see | ||
# https://github.com/python/mypy/issues/4195#issuecomment-341915031 | ||
lib_path.insert(0, os.getcwd()) | ||
if options.bazel: | ||
dir = '.' | ||
else: | ||
dir = os.getcwd() | ||
if dir not in lib_path: | ||
lib_path.insert(0, dir) | ||
|
||
# Prepend a config-defined mypy path. | ||
lib_path[:0] = options.mypy_path | ||
|
@@ -931,14 +961,17 @@ def get_cache_names(id: str, path: str, manager: BuildManager) -> Tuple[str, str | |
|
||
Args: | ||
id: module ID | ||
path: module path (used to recognize packages) | ||
path: module path | ||
cache_dir: cache directory | ||
pyversion: Python version (major, minor) | ||
|
||
Returns: | ||
A tuple with the file names to be used for the meta JSON and the | ||
data JSON, respectively. | ||
""" | ||
pair = manager.options.cache_map.get(path) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It feels naïvely like it'd be cleaner to always use the cache map in the presence of a cache map at all; I don't know that the hybrid mode of looking in a cache dir and a cache map file would be very meaningful, and it certainly sounds hard to debug. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That might well be the case but I haven't solved what to do about typeshed yet, so without keeping cache_dir as a fallback, typeshed will always read the source. |
||
if pair is not None: | ||
return pair | ||
cache_dir = manager.options.cache_dir | ||
pyversion = manager.options.python_version | ||
prefix = os.path.join(cache_dir, '%d.%d' % pyversion, *id.split('.')) | ||
|
@@ -1055,15 +1088,16 @@ def validate_meta(meta: Optional[CacheMeta], id: str, path: Optional[str], | |
manager.log('Metadata abandoned for {}: errors were previously ignored'.format(id)) | ||
return None | ||
|
||
bazel = manager.options.bazel | ||
assert path is not None, "Internal error: meta was provided without a path" | ||
# Check data_json; assume if its mtime matches it's good. | ||
# TODO: stat() errors | ||
data_mtime = getmtime(meta.data_json) | ||
data_mtime = 0 if bazel else getmtime(meta.data_json) | ||
if data_mtime != meta.data_mtime: | ||
manager.log('Metadata abandoned for {}: data cache is modified'.format(id)) | ||
return None | ||
|
||
path = os.path.abspath(path) | ||
path = relpath(path) if bazel else os.path.abspath(path) | ||
try: | ||
st = manager.get_stat(path) | ||
except OSError: | ||
|
@@ -1083,11 +1117,11 @@ def validate_meta(meta: Optional[CacheMeta], id: str, path: Optional[str], | |
return meta | ||
|
||
size = st.st_size | ||
if size != meta.size: | ||
if not bazel and size != meta.size: | ||
manager.log('Metadata abandoned for {}: file {} has different size'.format(id, path)) | ||
return None | ||
|
||
mtime = int(st.st_mtime) | ||
mtime = 0 if bazel else int(st.st_mtime) | ||
if mtime != meta.mtime or path != meta.path: | ||
try: | ||
source_hash = manager.fscache.md5(path) | ||
|
@@ -1171,8 +1205,18 @@ def write_cache(id: str, path: str, tree: MypyFile, | |
corresponding to the metadata that was written (the latter may | ||
be None if the cache could not be written). | ||
""" | ||
# For Bazel we use relative paths and zero mtimes. | ||
bazel = manager.options.bazel | ||
|
||
# Obtain file paths | ||
path = os.path.abspath(path) | ||
if not bazel: | ||
# Normally, make all paths absolute. | ||
path = os.path.abspath(path) | ||
else: | ||
# For Bazel, make all paths relative (else Bazel caching is thwarted). | ||
# And also patch tree.path, which might have been made absolute earlier. | ||
tree.path = path = relpath(path) | ||
|
||
meta_json, data_json = get_cache_names(id, path, manager) | ||
manager.log('Writing {} {} {} {}'.format(id, path, meta_json, data_json)) | ||
|
||
|
@@ -1192,7 +1236,8 @@ def write_cache(id: str, path: str, tree: MypyFile, | |
|
||
# Obtain and set up metadata | ||
try: | ||
os.makedirs(parent, exist_ok=True) | ||
if parent: | ||
os.makedirs(parent, exist_ok=True) | ||
st = manager.get_stat(path) | ||
except OSError as err: | ||
manager.log("Cannot get stat for {}: {}".format(path, err)) | ||
|
@@ -1207,10 +1252,11 @@ def write_cache(id: str, path: str, tree: MypyFile, | |
return interface_hash, None | ||
|
||
# Write data cache file, if applicable | ||
# Note that for Bazel we don't record the data file's mtime. | ||
if old_interface_hash == interface_hash: | ||
# If the interface is unchanged, the cached data is guaranteed | ||
# to be equivalent, and we only need to update the metadata. | ||
data_mtime = getmtime(data_json) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ^ |
||
data_mtime = 0 if bazel else getmtime(data_json) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe put "0 if bazel else getmtime(path)" into a one-line function? (it appears in a lot of places and causes a bit of cognitive load each time). |
||
manager.trace("Interface for {} is unchanged".format(id)) | ||
else: | ||
manager.trace("Interface for {} has changed".format(id)) | ||
|
@@ -1227,9 +1273,9 @@ def write_cache(id: str, path: str, tree: MypyFile, | |
# Both have the effect of slowing down the next run a | ||
# little bit due to an out-of-date cache file. | ||
return interface_hash, None | ||
data_mtime = getmtime(data_json) | ||
data_mtime = 0 if bazel else getmtime(data_json) | ||
|
||
mtime = int(st.st_mtime) | ||
mtime = 0 if bazel else int(st.st_mtime) | ||
size = st.st_size | ||
options = manager.options.clone_for_module(id) | ||
assert source_hash is not None | ||
|
@@ -1271,7 +1317,7 @@ def delete_cache(id: str, path: str, manager: BuildManager) -> None: | |
This avoids inconsistent states with cache files from different mypy runs, | ||
see #4043 for an example. | ||
""" | ||
path = os.path.abspath(path) | ||
path = relpath(path) if manager.options.bazel else os.path.abspath(path) | ||
meta_json, data_json = get_cache_names(id, path, manager) | ||
manager.log('Deleting {} {} {} {}'.format(id, path, meta_json, data_json)) | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -367,6 +367,7 @@ def add_invertible_flag(flag: str, | |
parser.add_argument('--shadow-file', nargs=2, metavar=('SOURCE_FILE', 'SHADOW_FILE'), | ||
dest='shadow_file', | ||
help='Typecheck SHADOW_FILE in place of SOURCE_FILE.') | ||
|
||
# hidden options | ||
# --debug-cache will disable any cache-related compressions/optimizations, | ||
# which will make the cache writing process output pretty-printed JSON (which | ||
|
@@ -381,6 +382,15 @@ def add_invertible_flag(flag: str, | |
# --local-partial-types disallows partial types spanning module top level and a function | ||
# (implicitly defined in fine-grained incremental mode) | ||
parser.add_argument('--local-partial-types', action='store_true', help=argparse.SUPPRESS) | ||
# --bazel changes some behaviors for use with Bazel (https://bazel.build). | ||
parser.add_argument('--bazel', action='store_true', help=argparse.SUPPRESS) | ||
# --cache-map FILE ... gives a mapping from source files to cache files. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I am interested in what happened to the idea of dropping cache files next to source files. I remember choosing the same option you did here, but I forget why, because now-me had the idea that cache files next to source would be cleaner for some reason. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I tried to write this up in my overly long status report in #3912 -- Dropbox's build environment puts the output files in a different directory than the source files. It seems outputs (and inputs) must be There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That is correct -- you cannot add anything via ctx.files.srcs. You should assume that the source tree is read-only. If you declare a file, you must create it within the action (Bazel checks this). |
||
# Each triple of arguments is a source file, a cache meta file, and a cache data file. | ||
# Modules not mentioned in the file will go through cache_dir. | ||
# Must be followed by another flag or by '--' (and then only file args may follow). | ||
parser.add_argument('--cache-map', nargs='+', dest='special-opts:cache_map', | ||
help=argparse.SUPPRESS) | ||
|
||
# deprecated options | ||
parser.add_argument('--disallow-any', dest='special-opts:disallow_any', | ||
help=argparse.SUPPRESS) | ||
|
@@ -520,6 +530,18 @@ def add_invertible_flag(flag: str, | |
report_dir = val | ||
options.report_dirs[report_type] = report_dir | ||
|
||
# Parse cache map. Uses assertions for checking. | ||
if special_opts.cache_map: | ||
n = len(special_opts.cache_map) | ||
assert n % 3 == 0, "--cache-map requires one or more triples (see source)" | ||
for i in range(0, n, 3): | ||
source, meta_file, data_file = special_opts.cache_map[i:i + 3] | ||
assert source not in options.cache_map | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In final code probably wants to be something other than an assert |
||
assert source.endswith('.py') or source.endswith('.pyi'), source | ||
assert meta_file.endswith('.meta.json'), meta_file | ||
assert data_file.endswith('.data.json'), data_file | ||
options.cache_map[source] = (meta_file, data_file) | ||
|
||
# Let quick_and_dirty imply incremental. | ||
if options.quick_and_dirty: | ||
options.incremental = True | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Dead code?