Skip to content

Commit c6c99b9

Browse files
authored
[mypyc] Make a bunch of the build process more configurable (#7939)
1 parent 8d562e2 commit c6c99b9

File tree

7 files changed

+93
-68
lines changed

7 files changed

+93
-68
lines changed

mypyc/build.py

Lines changed: 52 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -18,24 +18,25 @@
1818
hackily decide based on whether setuptools has been imported already.
1919
"""
2020

21-
import glob
2221
import sys
2322
import os.path
2423
import hashlib
2524
import time
2625
import re
2726

28-
from typing import List, Tuple, Any, Optional, Dict, Union, Set, cast
27+
from typing import List, Tuple, Any, Optional, Dict, Union, Set, Iterable, cast
2928
from typing_extensions import TYPE_CHECKING, NoReturn, Type
3029

3130
from mypy.main import process_options
3231
from mypy.errors import CompileError
3332
from mypy.options import Options
3433
from mypy.build import BuildSource
34+
from mypy.fscache import FileSystemCache
35+
3536
from mypyc.namegen import exported_name
3637
from mypyc.options import CompilerOptions
3738
from mypyc.errors import Errors
38-
from mypyc.common import BUILD_DIR, shared_lib_name
39+
from mypyc.common import shared_lib_name
3940
from mypyc.ops import format_modules
4041

4142
from mypyc import emitmodule
@@ -78,16 +79,21 @@ def fail(message: str) -> NoReturn:
7879
sys.exit(message)
7980

8081

81-
def get_mypy_config(paths: List[str],
82-
mypy_options: Optional[List[str]],
83-
compiler_options: CompilerOptions) -> Tuple[List[BuildSource], Options]:
82+
def get_mypy_config(mypy_options: List[str],
83+
only_compile_paths: Optional[Iterable[str]],
84+
compiler_options: CompilerOptions,
85+
fscache: Optional[FileSystemCache],
86+
) -> Tuple[List[BuildSource], List[BuildSource], Options]:
8487
"""Construct mypy BuildSources and Options from file and options lists"""
85-
# It is kind of silly to do this but oh well
86-
mypy_options = mypy_options or []
87-
mypy_options.append('--')
88-
mypy_options.extend(paths)
88+
all_sources, options = process_options(mypy_options, fscache=fscache)
89+
if only_compile_paths:
90+
paths_set = set(only_compile_paths)
91+
mypyc_sources = [s for s in all_sources if s.path in paths_set]
92+
else:
93+
mypyc_sources = all_sources
8994

90-
sources, options = process_options(mypy_options)
95+
if not mypyc_sources:
96+
return mypyc_sources, all_sources, options
9197

9298
# Override whatever python_version is inferred from the .ini file,
9399
# and set the python_version to be the currently used version.
@@ -104,10 +110,10 @@ def get_mypy_config(paths: List[str],
104110
options.incremental = compiler_options.separate
105111
options.preserve_asts = True
106112

107-
for source in sources:
113+
for source in mypyc_sources:
108114
options.per_module_options.setdefault(source.module, {})['mypyc'] = True
109115

110-
return sources, options
116+
return mypyc_sources, all_sources, options
111117

112118

113119
shim_template = """\
@@ -170,6 +176,7 @@ def include_dir() -> str:
170176
def generate_c(sources: List[BuildSource],
171177
options: Options,
172178
groups: emitmodule.Groups,
179+
fscache: FileSystemCache,
173180
compiler_options: Optional[CompilerOptions] = None
174181
) -> Tuple[List[List[Tuple[str, str]]], str]:
175182
"""Drive the actual core compilation step.
@@ -185,7 +192,8 @@ def generate_c(sources: List[BuildSource],
185192
# Do the actual work now
186193
t0 = time.time()
187194
try:
188-
result = emitmodule.parse_and_typecheck(sources, options, groups)
195+
result = emitmodule.parse_and_typecheck(
196+
sources, options, compiler_options, groups, fscache)
189197
except CompileError as e:
190198
for line in e.messages:
191199
print(line)
@@ -195,10 +203,6 @@ def generate_c(sources: List[BuildSource],
195203
if compiler_options.verbose:
196204
print("Parsed and typechecked in {:.3f}s".format(t1 - t0))
197205

198-
all_module_names = []
199-
for group_sources, _ in groups:
200-
all_module_names.extend([source.module for source in group_sources])
201-
202206
errors = Errors()
203207

204208
modules, ctext = emitmodule.compile_modules_to_c(result,
@@ -293,6 +297,7 @@ def write_file(path: str, contents: str) -> None:
293297
except IOError:
294298
old_contents = None
295299
if old_contents != encoded_contents:
300+
os.makedirs(os.path.dirname(path), exist_ok=True)
296301
with open(path, 'wb') as f:
297302
f.write(encoded_contents)
298303

@@ -363,24 +368,29 @@ def get_header_deps(cfiles: List[Tuple[str, str]]) -> List[str]:
363368

364369
def mypycify(
365370
paths: List[str],
366-
mypy_options: Optional[List[str]] = None,
367371
*,
372+
only_compile_paths: Optional[Iterable[str]] = None,
368373
verbose: bool = False,
369374
opt_level: str = '3',
370375
strip_asserts: bool = False,
371376
multi_file: bool = False,
372377
separate: Union[bool, List[Tuple[List[str], Optional[str]]]] = False,
373-
skip_cgen_input: Optional[Any] = None
378+
skip_cgen_input: Optional[Any] = None,
379+
target_dir: Optional[str] = None,
380+
include_runtime_files: Optional[bool] = None
374381
) -> List['Extension']:
375382
"""Main entry point to building using mypyc.
376383
377384
This produces a list of Extension objects that should be passed as the
378385
ext_modules parameter to setup.
379386
380387
Arguments:
381-
paths: A list of file paths to build. It may contain globs.
382-
mypy_options: Optionally, a list of command line flags to pass to mypy.
383-
(This can also contain additional files, for compatibility reasons.)
388+
paths: A list of file paths to build. It may also contain mypy options.
389+
only_compile_paths: If not None, an iterable of paths that are to be
390+
the only modules compiled, even if other modules
391+
appear in the mypy command line given to paths.
392+
(These modules must still be passed to paths.)
393+
384394
verbose: Should mypyc be more verbose. Defaults to false.
385395
386396
opt_level: The optimization level, as a string. Defaults to '3' (meaning '-O3').
@@ -401,6 +411,11 @@ def mypycify(
401411
speed up compilation, but calls between groups can
402412
be slower than calls within a group and can't be
403413
inlined.
414+
target_dir: The directory to write C output files. Defaults to 'build'.
415+
include_runtime_files: If not None, whether the mypyc runtime library
416+
should be directly #include'd instead of linked
417+
separately in order to reduce compiler invocations.
418+
Defaults to False in multi_file mode, True otherwise.
404419
"""
405420

406421
setup_mypycify_vars()
@@ -409,6 +424,8 @@ def mypycify(
409424
multi_file=multi_file,
410425
verbose=verbose,
411426
separate=separate is not False,
427+
target_dir=target_dir,
428+
include_runtime_files=include_runtime_files,
412429
)
413430

414431
# Create a compiler object so we can make decisions based on what
@@ -417,32 +434,25 @@ def mypycify(
417434
compiler = ccompiler.new_compiler() # type: Any
418435
sysconfig.customize_compiler(compiler)
419436

420-
expanded_paths = []
421-
for path in paths:
422-
expanded_paths.extend(glob.glob(path))
423-
424-
build_dir = BUILD_DIR # TODO: can this be overridden??
425-
try:
426-
os.mkdir(build_dir)
427-
except FileExistsError:
428-
pass
437+
build_dir = compiler_options.target_dir
429438

430-
sources, options = get_mypy_config(expanded_paths, mypy_options, compiler_options)
439+
fscache = FileSystemCache()
440+
mypyc_sources, all_sources, options = get_mypy_config(
441+
paths, only_compile_paths, compiler_options, fscache)
431442
# We generate a shared lib if there are multiple modules or if any
432443
# of the modules are in package. (Because I didn't want to fuss
433444
# around with making the single module code handle packages.)
434-
use_shared_lib = len(sources) > 1 or any('.' in x.module for x in sources)
445+
use_shared_lib = len(mypyc_sources) > 1 or any('.' in x.module for x in mypyc_sources)
435446

436-
groups = construct_groups(sources, separate, use_shared_lib)
447+
groups = construct_groups(mypyc_sources, separate, use_shared_lib)
437448

438449
# We let the test harness just pass in the c file contents instead
439450
# so that it can do a corner-cutting version without full stubs.
440451
if not skip_cgen_input:
441-
group_cfiles, ops_text = generate_c(sources, options, groups,
452+
group_cfiles, ops_text = generate_c(all_sources, options, groups, fscache,
442453
compiler_options=compiler_options)
443454
# TODO: unique names?
444-
with open(os.path.join(build_dir, 'ops.txt'), 'w') as f:
445-
f.write(ops_text)
455+
write_file(os.path.join(build_dir, 'ops.txt'), ops_text)
446456
else:
447457
group_cfiles = skip_cgen_input
448458

@@ -487,10 +497,11 @@ def mypycify(
487497
'/wd9025', # warning about overriding /GL
488498
]
489499

490-
# In multi-file mode, copy the runtime library in.
491-
# Otherwise it just gets #included to save on compiler invocations
500+
# If configured to (defaults to yes in multi-file mode), copy the
501+
# runtime library in. Otherwise it just gets #included to save on
502+
# compiler invocations.
492503
shared_cfilenames = []
493-
if multi_file:
504+
if not compiler_options.include_runtime_files:
494505
for name in ['CPy.c', 'getargs.c']:
495506
rt_file = os.path.join(build_dir, name)
496507
with open(os.path.join(include_dir(), name), encoding='utf-8') as f:

mypyc/common.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@
22
if MYPY:
33
from typing_extensions import Final
44

5-
BUILD_DIR = 'build'
6-
75
PREFIX = 'CPyPy_' # type: Final # Python wrappers
86
NATIVE_PREFIX = 'CPyDef_' # type: Final # Native functions etc.
97
DUNDER_PREFIX = 'CPyDunder_' # type: Final # Wrappers for exposing dunder methods to the API

mypyc/emitmodule.py

Lines changed: 27 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,11 @@
1717
from mypy.errors import CompileError
1818
from mypy.options import Options
1919
from mypy.plugin import Plugin, ReportConfigContext
20+
from mypy.fscache import FileSystemCache
2021

2122
from mypyc import genops
2223
from mypyc.common import (
23-
BUILD_DIR, PREFIX, TOP_LEVEL_NAME, INT_PREFIX, MODULE_PREFIX, shared_lib_name,
24+
PREFIX, TOP_LEVEL_NAME, INT_PREFIX, MODULE_PREFIX, shared_lib_name,
2425
)
2526
from mypyc.emit import EmitterContext, Emitter, HeaderDeclaration
2627
from mypyc.emitfunc import generate_native_function, native_function_header
@@ -86,14 +87,16 @@ class MypycPlugin(Plugin):
8687
recompile the module so we mark it as stale.
8788
"""
8889

89-
def __init__(self, options: Options, groups: Groups) -> None:
90+
def __init__(
91+
self, options: Options, compiler_options: CompilerOptions, groups: Groups) -> None:
9092
super().__init__(options)
9193
self.group_map = {} # type: Dict[str, Tuple[Optional[str], List[str]]]
9294
for sources, name in groups:
9395
modules = sorted(source.module for source in sources)
9496
for id in modules:
9597
self.group_map[id] = (name, modules)
9698

99+
self.compiler_options = compiler_options
97100
self.metastore = create_metastore(options)
98101

99102
def report_config_data(
@@ -136,7 +139,7 @@ def report_config_data(
136139
# .mypy_cache, which we should handle gracefully.
137140
for path, hash in ir_data['src_hashes'].items():
138141
try:
139-
with open(os.path.join(BUILD_DIR, path), 'rb') as f:
142+
with open(os.path.join(self.compiler_options.target_dir, path), 'rb') as f:
140143
contents = f.read()
141144
except FileNotFoundError:
142145
return None
@@ -151,14 +154,20 @@ def get_additional_deps(self, file: MypyFile) -> List[Tuple[int, str, int]]:
151154
return [(10, id, -1) for id in self.group_map.get(file.fullname(), (None, []))[1]]
152155

153156

154-
def parse_and_typecheck(sources: List[BuildSource], options: Options,
155-
groups: Groups,
156-
alt_lib_path: Optional[str] = None) -> BuildResult:
157+
def parse_and_typecheck(
158+
sources: List[BuildSource],
159+
options: Options,
160+
compiler_options: CompilerOptions,
161+
groups: Groups,
162+
fscache: Optional[FileSystemCache] = None,
163+
alt_lib_path: Optional[str] = None
164+
) -> BuildResult:
157165
assert options.strict_optional, 'strict_optional must be turned on'
158166
result = build(sources=sources,
159167
options=options,
160168
alt_lib_path=alt_lib_path,
161-
extra_plugins=[MypycPlugin(options, groups)])
169+
fscache=fscache,
170+
extra_plugins=[MypycPlugin(options, compiler_options, groups)])
162171
if result.errors:
163172
raise CompileError(result.errors)
164173
return result
@@ -273,8 +282,9 @@ def compile_ir_to_c(
273282
continue
274283
literals = mapper.literals[group_name]
275284
generator = GroupGenerator(
276-
literals, group_modules, source_paths, group_name, mapper.group_map, names,
277-
compiler_options.multi_file
285+
literals, group_modules, source_paths,
286+
group_name, mapper.group_map, names,
287+
compiler_options
278288
)
279289
ctext[group_name] = generator.generate_c_for_modules()
280290

@@ -406,16 +416,13 @@ def generate_function_declaration(fn: FuncIR, emitter: Emitter) -> None:
406416

407417
def encode_as_c_string(s: str) -> Tuple[str, int]:
408418
"""Produce a utf-8 encoded, escaped, quoted C string and its size from a string"""
409-
# This is a kind of abusive way to do this...
410-
b = s.encode('utf-8')
411-
escaped = str(b)[2:-1].replace('"', '\\"')
412-
return '"{}"'.format(escaped), len(b)
419+
return encode_bytes_as_c_string(s.encode('utf-8'))
413420

414421

415422
def encode_bytes_as_c_string(b: bytes) -> Tuple[str, int]:
416423
"""Produce a single-escaped, quoted C string and its size from a bytes"""
417424
# This is a kind of abusive way to do this...
418-
escaped = str(b)[2:-1].replace('"', '\\"')
425+
escaped = repr(b)[2:-1].replace('"', '\\"')
419426
return '"{}"'.format(escaped), len(b)
420427

421428

@@ -438,7 +445,7 @@ def __init__(self,
438445
group_name: Optional[str],
439446
group_map: Dict[str, Optional[str]],
440447
names: NameGenerator,
441-
multi_file: bool) -> None:
448+
compiler_options: CompilerOptions) -> None:
442449
"""Generator for C source for a compilation group.
443450
444451
The code for a compilation group contains an internal and an
@@ -465,7 +472,8 @@ def __init__(self,
465472
self.simple_inits = [] # type: List[Tuple[str, str]]
466473
self.group_name = group_name
467474
self.use_shared_lib = group_name is not None
468-
self.multi_file = multi_file
475+
self.compiler_options = compiler_options
476+
self.multi_file = compiler_options.multi_file
469477

470478
@property
471479
def group_suffix(self) -> str:
@@ -476,10 +484,9 @@ def generate_c_for_modules(self) -> List[Tuple[str, str]]:
476484
multi_file = self.use_shared_lib and self.multi_file
477485

478486
base_emitter = Emitter(self.context)
479-
# When not in multi-file mode we just include the runtime
480-
# library c files to reduce the number of compiler invocations
481-
# needed
482-
if not self.multi_file:
487+
# Optionally just include the runtime library c files to
488+
# reduce the number of compiler invocations needed
489+
if self.compiler_options.include_runtime_files:
483490
base_emitter.emit_line('#include "CPy.c"')
484491
base_emitter.emit_line('#include "getargs.c"')
485492
base_emitter.emit_line('#include "__native{}.h"'.format(self.group_suffix))

mypyc/options.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,17 @@
1+
from typing import Optional
2+
3+
14
class CompilerOptions:
25
def __init__(self, strip_asserts: bool = False, multi_file: bool = False,
3-
verbose: bool = False, separate: bool = False) -> None:
6+
verbose: bool = False, separate: bool = False,
7+
target_dir: Optional[str] = None,
8+
include_runtime_files: Optional[bool] = None) -> None:
49
self.strip_asserts = strip_asserts
510
self.multi_file = multi_file
611
self.verbose = verbose
712
self.separate = separate
813
self.global_opts = not separate
14+
self.target_dir = target_dir or 'build'
15+
self.include_runtime_files = (
16+
include_runtime_files if include_runtime_files is not None else not multi_file
17+
)

mypyc/test/test_run.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -202,13 +202,14 @@ def run_case_step(self, testcase: DataDrivenTestCase, incremental_step: int) ->
202202
groups = construct_groups(sources, separate, len(module_names) > 1)
203203

204204
try:
205+
compiler_options = CompilerOptions(multi_file=self.multi_file, separate=self.separate)
205206
result = emitmodule.parse_and_typecheck(
206207
sources=sources,
207208
options=options,
209+
compiler_options=compiler_options,
208210
groups=groups,
209211
alt_lib_path='.')
210212
errors = Errors()
211-
compiler_options = CompilerOptions(multi_file=self.multi_file, separate=self.separate)
212213
ir, cfiles = emitmodule.compile_modules_to_c(
213214
result,
214215
compiler_options=compiler_options,

0 commit comments

Comments
 (0)