python module: add an automatic byte-compilation step

For all source `*.py` files installed via either py.install_sources() or
an `install_dir: py.get_install_dir()`, produce `*.pyc` files at install
time. Controllable via a module option.
pull/11530/head
Eli Schwartz 2 years ago
parent 4a2530802c
commit 0e7fb07f91
No known key found for this signature in database
GPG Key ID: CEB167EFB5722BD6
  1. 11
      docs/markdown/Builtin-options.md
  2. 4
      docs/markdown/snippets/python_bytecompile.md
  3. 2
      mesonbuild/coredata.py
  4. 65
      mesonbuild/modules/python.py
  5. 67
      mesonbuild/scripts/pycompile.py
  6. 2
      test cases/common/252 install data structured/meson.build
  7. 2
      test cases/python/2 extmodule/meson.build
  8. 3
      test cases/python/7 install path/meson.build

@ -351,6 +351,7 @@ install prefix. For example: if the install prefix is `/usr` and the
| Option | Default value | Possible values | Description | | Option | Default value | Possible values | Description |
| ------ | ------------- | ----------------- | ----------- | | ------ | ------------- | ----------------- | ----------- |
| bytecompile | 0 | integer from -1 to 2 | What bytecode optimization level to use (Since 1.2.0) |
| install_env | prefix | {auto,prefix,system,venv} | Which python environment to install to (Since 0.62.0) | | install_env | prefix | {auto,prefix,system,venv} | Which python environment to install to (Since 0.62.0) |
| platlibdir | | Directory path | Directory for site-specific, platform-specific files (Since 0.60.0) | | platlibdir | | Directory path | Directory for site-specific, platform-specific files (Since 0.60.0) |
| purelibdir | | Directory path | Directory for site-specific, non-platform-specific files (Since 0.60.0) | | purelibdir | | Directory path | Directory for site-specific, non-platform-specific files (Since 0.60.0) |
@ -373,3 +374,13 @@ installation is a virtualenv, and use `venv` or `system` as appropriate (but
never `prefix`). This option is mutually exclusive with the `platlibdir`/`purelibdir`. never `prefix`). This option is mutually exclusive with the `platlibdir`/`purelibdir`.
For backwards compatibility purposes, the default `install_env` is `prefix`. For backwards compatibility purposes, the default `install_env` is `prefix`.
*Since 1.2.0* The `python.bytecompile` option can be used to enable compiling
python bytecode. Bytecode has 3 optimization levels:
- 0, bytecode without optimizations
- 1, bytecode with some optimizations
- 2, bytecode with some more optimizations
To this, Meson adds level `-1`, which is to not attempt to compile bytecode at
all.

@ -0,0 +1,4 @@
## Python module can now compile bytecode
A new builtin option is available: `-Dpython.bytecompile=2`. It can be used to
compile bytecode for all pure python files installed via the python module.

@ -1266,6 +1266,8 @@ BUILTIN_CORE_OPTIONS: 'MutableKeyedOptionDictType' = OrderedDict([
BuiltinOption(UserBooleanOption, 'Generate pkgconfig files as relocatable', False)), BuiltinOption(UserBooleanOption, 'Generate pkgconfig files as relocatable', False)),
# Python module # Python module
(OptionKey('bytecompile', module='python'),
BuiltinOption(UserIntegerOption, 'Whether to compile bytecode', (-1, 2, 0))),
(OptionKey('install_env', module='python'), (OptionKey('install_env', module='python'),
BuiltinOption(UserComboOption, 'Which python environment to install to', 'prefix', choices=['auto', 'prefix', 'system', 'venv'])), BuiltinOption(UserComboOption, 'Which python environment to install to', 'prefix', choices=['auto', 'prefix', 'system', 'venv'])),
(OptionKey('platlibdir', module='python'), (OptionKey('platlibdir', module='python'),

@ -13,9 +13,7 @@
# limitations under the License. # limitations under the License.
from __future__ import annotations from __future__ import annotations
import copy import copy, json, os, shutil
import os
import shutil
import typing as T import typing as T
from . import ExtensionModule, ModuleInfo from . import ExtensionModule, ModuleInfo
@ -41,7 +39,7 @@ if T.TYPE_CHECKING:
from typing_extensions import TypedDict from typing_extensions import TypedDict
from . import ModuleState from . import ModuleState
from ..build import SharedModule, Data from ..build import Build, SharedModule, Data
from ..dependencies import Dependency from ..dependencies import Dependency
from ..interpreter import Interpreter from ..interpreter import Interpreter
from ..interpreter.kwargs import ExtractRequired from ..interpreter.kwargs import ExtractRequired
@ -66,6 +64,12 @@ mod_kwargs -= {'name_prefix', 'name_suffix'}
class PythonExternalProgram(BasicPythonExternalProgram): class PythonExternalProgram(BasicPythonExternalProgram):
# This is a ClassVar instead of an instance bool, because although an
# installation is cached, we actually copy it, modify attributes such as pure,
# and return a temporary one rather than the cached object.
run_bytecompile: T.ClassVar[T.Dict[str, bool]] = {}
def sanity(self, state: T.Optional['ModuleState'] = None) -> bool: def sanity(self, state: T.Optional['ModuleState'] = None) -> bool:
ret = super().sanity() ret = super().sanity()
if ret: if ret:
@ -216,6 +220,7 @@ class PythonInstallation(ExternalProgramHolder):
) )
def install_sources_method(self, args: T.Tuple[T.List[T.Union[str, mesonlib.File]]], def install_sources_method(self, args: T.Tuple[T.List[T.Union[str, mesonlib.File]]],
kwargs: 'PyInstallKw') -> 'Data': kwargs: 'PyInstallKw') -> 'Data':
self.held_object.run_bytecompile[self.version] = True
tag = kwargs['install_tag'] or 'python-runtime' tag = kwargs['install_tag'] or 'python-runtime'
pure = kwargs['pure'] if kwargs['pure'] is not None else self.pure pure = kwargs['pure'] if kwargs['pure'] is not None else self.pure
install_dir = self._get_install_dir_impl(pure, kwargs['subdir']) install_dir = self._get_install_dir_impl(pure, kwargs['subdir'])
@ -229,6 +234,7 @@ class PythonInstallation(ExternalProgramHolder):
@noPosargs @noPosargs
@typed_kwargs('python_installation.install_dir', _PURE_KW, _SUBDIR_KW) @typed_kwargs('python_installation.install_dir', _PURE_KW, _SUBDIR_KW)
def get_install_dir_method(self, args: T.List['TYPE_var'], kwargs: 'PyInstallKw') -> str: def get_install_dir_method(self, args: T.List['TYPE_var'], kwargs: 'PyInstallKw') -> str:
self.held_object.run_bytecompile[self.version] = True
pure = kwargs['pure'] if kwargs['pure'] is not None else self.pure pure = kwargs['pure'] if kwargs['pure'] is not None else self.pure
return self._get_install_dir_impl(pure, kwargs['subdir']) return self._get_install_dir_impl(pure, kwargs['subdir'])
@ -297,6 +303,56 @@ class PythonModule(ExtensionModule):
'find_installation': self.find_installation, 'find_installation': self.find_installation,
}) })
def _get_install_scripts(self) -> T.List[mesonlib.ExecutableSerialisation]:
backend = self.interpreter.backend
ret = []
optlevel = self.interpreter.environment.coredata.get_option(mesonlib.OptionKey('bytecompile', module='python'))
if optlevel == -1:
return ret
if not any(PythonExternalProgram.run_bytecompile.values()):
return ret
installdata = backend.create_install_data()
py_files = []
def should_append(f, isdir: bool = False):
# This uses the install_plan decorated names to see if the original source was propagated via
# install_sources() or get_install_dir().
return f.startswith(('{py_platlib}', '{py_purelib}')) and (f.endswith('.py') or isdir)
for t in installdata.targets:
if should_append(t.out_name):
py_files.append(os.path.join(installdata.prefix, t.outdir, os.path.basename(t.fname)))
for d in installdata.data:
if should_append(d.install_path_name):
py_files.append(os.path.join(installdata.prefix, d.install_path))
for d in installdata.install_subdirs:
if should_append(d.install_path_name, True):
py_files.append(os.path.join(installdata.prefix, d.install_path))
import importlib.resources
pycompile = os.path.join(self.interpreter.environment.get_scratch_dir(), 'pycompile.py')
with open(pycompile, 'wb') as f:
f.write(importlib.resources.read_binary('mesonbuild.scripts', 'pycompile.py'))
for i in self.installations.values():
if isinstance(i, PythonExternalProgram) and i.run_bytecompile[i.info['version']]:
i = T.cast(PythonExternalProgram, i)
manifest = f'python-{i.info["version"]}-installed.json'
manifest_json = []
for f in py_files:
if f.startswith((os.path.join(installdata.prefix, i.platlib), os.path.join(installdata.prefix, i.purelib))):
manifest_json.append(f)
with open(os.path.join(self.interpreter.environment.get_scratch_dir(), manifest), 'w', encoding='utf-8') as f:
json.dump(manifest_json, f)
cmd = i.command + [pycompile, manifest, str(optlevel)]
script = backend.get_executable_serialisation(cmd, verbose=True)
ret.append(script)
return ret
def postconf_hook(self, b: Build) -> None:
b.install_scripts.extend(self._get_install_scripts())
# https://www.python.org/dev/peps/pep-0397/ # https://www.python.org/dev/peps/pep-0397/
@staticmethod @staticmethod
def _get_win_pythonpath(name_or_path: str) -> T.Optional[str]: def _get_win_pythonpath(name_or_path: str) -> T.Optional[str]:
@ -421,6 +477,7 @@ class PythonModule(ExtensionModule):
else: else:
python = copy.copy(python) python = copy.copy(python)
python.pure = kwargs['pure'] python.pure = kwargs['pure']
python.run_bytecompile.setdefault(python.info['version'], False)
return python return python
raise mesonlib.MesonBugException('Unreachable code was reached (PythonModule.find_installation).') raise mesonlib.MesonBugException('Unreachable code was reached (PythonModule.find_installation).')

@ -0,0 +1,67 @@
# Copyright 2016 The Meson development team
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ignore all lints for this file, since it is run by python2 as well
# type: ignore
# pylint: disable=deprecated-module
import json, os, subprocess, sys
from compileall import compile_file
destdir = os.environ.get('DESTDIR')
quiet = int(os.environ.get('MESON_INSTALL_QUIET', 0))
def destdir_join(d1, d2):
if not d1:
return d2
# c:\destdir + c:\prefix must produce c:\destdir\prefix
parts = os.path.splitdrive(d2)
return d1 + parts[1]
def compileall(files):
for f in files:
if destdir is not None:
ddir = os.path.dirname(f)
fullpath = destdir_join(destdir, f)
else:
ddir = None
fullpath = f
if os.path.isdir(fullpath):
for root, _, files in os.walk(fullpath):
ddir = os.path.dirname(os.path.splitdrive(f)[0] + root[len(destdir):])
for dirf in files:
if dirf.endswith('.py'):
fullpath = os.path.join(root, dirf)
compile_file(fullpath, ddir, force=True, quiet=quiet)
else:
compile_file(fullpath, ddir, force=True, quiet=quiet)
def run(manifest):
data_file = os.path.join(os.path.dirname(__file__), manifest)
with open(data_file, 'rb') as f:
dat = json.load(f)
compileall(dat)
if __name__ == '__main__':
manifest = sys.argv[1]
run(manifest)
if len(sys.argv) > 2:
optlevel = int(sys.argv[2])
# python2 only needs one or the other
if optlevel == 1 or (sys.version_info >= (3,) and optlevel > 0):
subprocess.check_call([sys.executable, '-O'] + sys.argv[:2])
if optlevel == 2:
subprocess.check_call([sys.executable, '-OO'] + sys.argv[:2])

@ -1,4 +1,4 @@
project('install structured data') project('install structured data', default_options: ['python.bytecompile=-1'])
install_data( install_data(
'dir1/file1', 'dir1/file1',

@ -1,5 +1,5 @@
project('Python extension module', 'c', project('Python extension module', 'c',
default_options : ['buildtype=release', 'werror=true']) default_options : ['buildtype=release', 'werror=true', 'python.bytecompile=-1'])
# Because Windows Python ships only with optimized libs, # Because Windows Python ships only with optimized libs,
# we must build this project the same way. # we must build this project the same way.

@ -1,7 +1,8 @@
project('install path', project('install path',
default_options: [ default_options: [
'python.bytecompile=-1',
'python.purelibdir=/pure', 'python.purelibdir=/pure',
'python.platlibdir=/plat' 'python.platlibdir=/plat',
] ]
) )

Loading…
Cancel
Save